Google Play游戏服务多层设备的方向更改使用户离开房间 [英] Google Play Game Services Multi-Player Device Orientation change kicks user out of room

查看:139
本文介绍了Google Play游戏服务多层设备的方向更改使用户离开房间的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我正在开发一个只有一个活动(扩展了 BaseGameActivity )的应用程序,并且可以在多个片段之间切换(很像Google的示例代码状态).

我现在正在2个单独的设备上测试多人游戏.两个用户都可以成功登录,相互发送消息等.但是,一旦一个用户旋转设备,他们就会被踢出房间.

我认为这是有道理的,因为该活动已被破坏并重新创建.但是我不明白的是我们需要做些什么才能允许用户旋转设备并保持游戏状态(登录,加入房间等)完好无损?

  • 一个想法:android:configChanged ="orientation | screenSize"-但是Android不鼓励这样做(在大多数情况下,有充分的理由)-但这是我们必须与Google Play Game Services保持联系的方式设备方向改变的房间?

  • 使用"onRetainNonConfigurationInstance()"保存GameHelper实例,并在重新创建活动时再次使用它,该怎么办?

  • 还是以某种方式在服务中实现游戏连接(登录,加入房间等)?

还是我在想这一切错误的方式?感谢您的想法&帮助.如果可能,代码示例也将不胜感激.

解决方案

感谢@Sheldon在无头"片段上为setRetainInstance(true)指出正确的方向.这就是我解决这个问题的方法,现在我想在这里粘贴代码,希望对其他人有所帮助.但首先:

口头解释

如问题中所述,设备方向更改将破坏MainActivity extends BaseGameActivity,并破坏您的游戏状态(即您与Google Play服务的连接).但是,我们可以将所有GameHelper代码放入声明为setRetainInstance(true)的无头"片段(无UI的片段)中.现在,当我们的MainActivity extends FragmentActivity因方向改变而被破坏时,无头碎片被阻止甚至脱落,但未被破坏! (未调用onDestroy())由Android重新创建MainActivity时,我们的无头片段会自动重新附加到其上.这时,在我们的无头片段中,未调用onCreate().所以onCreate()是我们连接GameHelper的地方.我们可以从onDestroy()中断开与GameHelper的连接,因为它将永远不会被调用,除非 Application 完成时(此时可以终止我们的连接).

注意:我认为GameHeaderFragment.java应该应该分解成一个Abstract类和一个继承自它的游戏特定类(但我在这里没有这样做).

这就是我想出的(请原谅我特定于游戏的代码交织的区域):

GameHeaderFragment.java

public class GameHelperFragment extends Fragment implements GameHelperListener, OnInvitationReceivedListener, RoomUpdateListener, RoomStatusUpdateListener, RealTimeMessageReceivedListener {

    protected MainActivity mActivity = null;

    // The game helper object. This class is mainly a wrapper around this object.
    protected GameHelper mHelper;

    final static int MAX_NUM_PLAYERS = 4;

    // Request codes for the UIs that we show with startActivityForResult:
    final static int RC_SELECT_PLAYERS = 10000;
    final static int RC_INVITATION_INBOX = 10001;
    final static int RC_WAITING_ROOM = 10002;

    // We expose these constants here because we don't want users of this class
    // to have to know about GameHelper at all.
    public static final int CLIENT_GAMES = GameHelper.CLIENT_GAMES;
    public static final int CLIENT_APPSTATE = GameHelper.CLIENT_APPSTATE;
    public static final int CLIENT_PLUS = GameHelper.CLIENT_PLUS;
    public static final int CLIENT_ALL = GameHelper.CLIENT_ALL;

    // Requested clients. By default, that's just the games client.
    protected int mRequestedClients = CLIENT_GAMES;

    protected String mSigningInMessage = "Signing in with Google";
    protected String mSigningOutMessage = "Signing out";

    // Custom Members
    String mMyId = "";
    String mRoomId = "";
    ArrayList<Participant> mParticipants = null;

    int mCurrentlyPlayingIdx = 0;  // idx into mParticipants
    boolean mIsMultiplayer = false;
    boolean mWaitRoomDismissedFromCode = false;

    public interface GameHelperFragmentListener {
        void onSignInFailed();
        void onSignInSucceeded();
        void onInvitationReceived(Invitation invitation);
        void showMainMenu();
        void showWaitScreen();
        void startGame();
        void participantLeftAtIdx(int idx);
        void handleRealTimeMessage(RealTimeMessage rtm);
    }

    GameHelperFragmentListener mListener;

    public GameHelperFragment() {
        super();
        Log.d("mab", "GHFrag.Constructor()");
    }

    /**
     * Sets the requested clients. The preferred way to set the requested clients is
     * via the constructor, but this method is available if for some reason your code
     * cannot do this in the constructor. This must be called before onCreate in order to
     * have any effect. If called after onCreate, this method is a no-op.
     *
     * @param requestedClients A combination of the flags CLIENT_GAMES, CLIENT_PLUS
     *         and CLIENT_APPSTATE, or CLIENT_ALL to request all available clients.
     */
    protected void setRequestedClients(int requestedClients) {
        mRequestedClients = requestedClients;
    }

    @Override
    public void onAttach(Activity activity) {
        Log.d("mab", this + ": onAttach(" + activity + ")");
        super.onAttach(activity);
        mActivity = (MainActivity) activity;
        mListener = (GameHelperFragmentListener) activity;
    }

    @Override
    public void onCreate(Bundle b) {
        Log.d("mab", this + ": onCreate()");
        super.onCreate(b);
        setRetainInstance(true);
        mHelper = new GameHelper(mActivity);
        mHelper.setup(this, mRequestedClients);  //'this' => GameHelperListener

        mHelper.setSigningInMessage(mSigningInMessage);
        mHelper.setSigningOutMessage(mSigningOutMessage);
        mHelper.onStart(mActivity);

    }


    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        return null;  // Headless Fragment
    }

    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        Log.d("mab", this + ": onActivityCreated()");
        super.onActivityCreated(savedInstanceState);
    }

    @Override
    public void onDestroy() {
        Log.d("mab", this + ": onDestroy()");
        super.onDestroy();
        mHelper.onStop();
    }

    @Override
    public void onActivityResult(int requestCode, int responseCode, Intent data) {
        Log.d("mab", this + ": onActivityResult(" + requestCode + ")");
        super.onActivityResult(requestCode, responseCode, data);
        mHelper.onActivityResult(requestCode, responseCode, data);

        switch (requestCode) {
        case RC_SELECT_PLAYERS:
            // we got the result from the "select players" UI -- ready to create the room
            handleSelectPlayersResult(responseCode, data);
            break;
        case RC_INVITATION_INBOX:
            // we got the result from the "select invitation" UI (invitation inbox). We're
            // ready to accept the selected invitation:
            handleInvitationInboxResult(responseCode, data);
            break;
        case RC_WAITING_ROOM:
            // ignore result if we dismissed the waiting room from code:
            if (mWaitRoomDismissedFromCode) break;

            // we got the result from the "waiting room" UI.
            if (responseCode == Activity.RESULT_OK) {

            } else if (responseCode == GamesActivityResultCodes.RESULT_LEFT_ROOM) {
                // player actively indicated that they want to leave the room
                leaveRoom();
            } else if (responseCode == Activity.RESULT_CANCELED) {
                leaveRoom();
            }

            break;
        }
    }

    // Handle the result of the "Select players UI" we launched when the user clicked the
    // "Invite friends" button. We react by creating a room with those players.
    private void handleSelectPlayersResult(int responseCode, Intent data) {
        if (responseCode != Activity.RESULT_OK) {
            Log.w("mab", "*** select players UI cancelled, " + responseCode);
            showMainMenu();
            return;
        }

        Log.d("mab", "Select players UI succeeded.");

        // get the invitee list
        final ArrayList<String> invitees = data.getStringArrayListExtra(GamesClient.EXTRA_PLAYERS);
        Log.d("mab", "Invitee count: " + invitees.size());

        // get the automatch criteria
        Bundle autoMatchCriteria = null;
        int minAutoMatchPlayers = data.getIntExtra(GamesClient.EXTRA_MIN_AUTOMATCH_PLAYERS, 0);
        int maxAutoMatchPlayers = data.getIntExtra(GamesClient.EXTRA_MAX_AUTOMATCH_PLAYERS, 0);
        if (minAutoMatchPlayers > 0 || maxAutoMatchPlayers > 0) {
            autoMatchCriteria = RoomConfig.createAutoMatchCriteria(
                    minAutoMatchPlayers, maxAutoMatchPlayers, 0);
            Log.d("mab", "Automatch criteria: " + autoMatchCriteria);
        }

        // create the room
        Log.d("mab", "Creating room...");
        RoomConfig.Builder rtmConfigBuilder = RoomConfig.builder(this);
        rtmConfigBuilder.addPlayersToInvite(invitees);
        rtmConfigBuilder.setMessageReceivedListener(this);
        rtmConfigBuilder.setRoomStatusUpdateListener(this);
        if (autoMatchCriteria != null) {
            rtmConfigBuilder.setAutoMatchCriteria(autoMatchCriteria);
        }
        showWaitScreen();

        keepScreenOn();
        getGamesClient().createRoom(rtmConfigBuilder.build());
        Log.d("mab", "Room configured, waiting for it to be created...");
    }

    // Handle the result of the invitation inbox UI, where the player can pick an invitation
    // to accept. We react by accepting the selected invitation, if any.
    private void handleInvitationInboxResult(int response, Intent data) {
        if (response != Activity.RESULT_OK) {
            Log.d("mab", "*** invitation inbox UI cancelled, " + response);
            showMainMenu();
            return;
        }

        Log.d("mab", "Invitation inbox UI succeeded.");
        Invitation inv = data.getExtras().getParcelable(GamesClient.EXTRA_INVITATION);

        // accept invitation
        acceptInviteToRoom(inv.getInvitationId());
    }

    protected GamesClient getGamesClient() {
        return mHelper.getGamesClient();
    }

    protected AppStateClient getAppStateClient() {
        return mHelper.getAppStateClient();
    }

    protected PlusClient getPlusClient() {
        return mHelper.getPlusClient();
    }

    protected boolean isSignedIn() {
        return mHelper.isSignedIn();
    }

    protected void beginUserInitiatedSignIn() {
        mHelper.beginUserInitiatedSignIn();
    }

    protected void signOut() {
        mHelper.signOut();
    }

    protected void showAlert(String title, String message) {
        mHelper.showAlert(title, message);
    }

    protected void showAlert(String message) {
        mHelper.showAlert(message);
    }

    protected void enableDebugLog(boolean enabled, String tag) {
        mHelper.enableDebugLog(enabled, tag);
    }

    protected String getInvitationId() {
        return mHelper.getInvitationId();
    }

    protected void reconnectClients(int whichClients) {
        mHelper.reconnectClients(whichClients);
    }

    protected String getScopes() {
        return mHelper.getScopes();
    }

    protected boolean hasSignInError() {
        return mHelper.hasSignInError();
    }

    protected ConnectionResult getSignInError() {
        return mHelper.getSignInError();
    }

    protected void setSignInMessages(String signingInMessage, String signingOutMessage) {
        mSigningInMessage = signingInMessage;
        mSigningOutMessage = signingOutMessage;
    }

    public void setRoomId(String rid) {
        mRoomId = rid;
    }
    public String getRoomId() {
        return mRoomId;
    }

    @Override
    public void onRealTimeMessageReceived(RealTimeMessage rtm) {
        mListener.handleRealTimeMessage(rtm);
    }

    // Called when we are connected to the room. We're not ready to play yet! (maybe not everybody is connected yet).
    @Override
    public void onConnectedToRoom(Room room) {

        Log.d("mab", "onConnectedToRoom.");

        // get room ID, participants and my ID:
        mRoomId = room.getRoomId();
        mParticipants = room.getParticipants();
        mMyId = room.getParticipantId(getGamesClient().getCurrentPlayerId());

        // print out the list of participants (for debug purposes)
        Log.d("mab", "Room ID: " + mRoomId);
        Log.d("mab", "My ID " + mMyId);
        Log.d("mab", "<< CONNECTED TO ROOM>>");
        Log.d("mab", "  Number of Joined Participants: " + getNumJoinedParticipants());
    }

    // Called when we get disconnected from the room. We return to the main screen.
    @Override
    public void onDisconnectedFromRoom(Room room) {
        mIsMultiplayer = false;
        mRoomId = null;
        showGameError("Disconnected from room");
    }


    @Override
    public void onJoinedRoom(int statusCode, Room room) {
        Log.d("mab", "onJoinedRoom(" + statusCode + ")");
        if (room != null) { Log.d("mab", " roomId: " + room.getRoomId()); }
        if (statusCode != GamesClient.STATUS_OK) {
            mIsMultiplayer = false;
            Log.e("mab", "*** Error: onJoinedRoom, status " + statusCode);
            showGameError("Joined room unsuccessfully: " + statusCode);
            return;
        }
        mRoomId = room.getRoomId();

        // show the waiting room UI
        showWaitingRoom(room);
    }

    // Called when we've successfully left the room (this happens a result of voluntarily leaving
    // via a call to leaveRoom(). If we get disconnected, we get onDisconnectedFromRoom()).
    @Override
    public void onLeftRoom(int statusCode, String roomId) {
        // we have left the room; return to main screen.
        Log.d("mab", "onLeftRoom, code " + statusCode);

        mRoomId = null;  //????? right?

        showMainMenu();
    }

    // Called when room is fully connected.
    @Override
    public void onRoomConnected(int statusCode, Room room) {
        Log.d("mab", "onRoomConnected(" + statusCode + ")");
        if (room != null) { Log.d("mab", " roomId: " + room.getRoomId()); }
        if (statusCode != GamesClient.STATUS_OK) {
            mIsMultiplayer = false;
            Log.d("mab", "*** Error: onRoomConnected, status " + statusCode);
            showGameError("Roon connected unsuccessfully: " + statusCode);
            return;
        }
        mRoomId = room.getRoomId();

        mParticipants = room.getParticipants();  // not sure if we need this here again, but shouldn't hurt (or maybe we want this ONLY here)
        mIsMultiplayer = true;

        // Set 1st player to take a turn
        mCurrentlyPlayingIdx = 0;

        // Start Game!
        mListener.startGame();

    }

    // Called when room has been created
    @Override
    public void onRoomCreated(int statusCode, Room room) {
        Log.d("mab", "onRoomCreated(" + statusCode + ")");
        if (room != null) { Log.d("mab", " roomId: " + room.getRoomId()); }
        if (statusCode != GamesClient.STATUS_OK) {
            mIsMultiplayer = false;
            Log.e("mab", "*** Error: onRoomCreated, status " + statusCode);
            showGameError("Room not created successfully: " + statusCode);
            return;
        }
        mRoomId = room.getRoomId();

        // show the waiting room UI
        showWaitingRoom(room);
    }

    // Called when we get an invitation to play a game. We react by showing that to the user.
    @Override
    public void onInvitationReceived(Invitation invitation) {
        Log.d("mab", "ghFrag.onInvitationReceived()");

        mListener.onInvitationReceived(invitation);
    }

    @Override
    public void onSignInFailed() {
        mListener.onSignInFailed();
    }

    @Override
    public void onSignInSucceeded() {
        // install invitation listener so we get notified if we receive an invitation to play a game.
        getGamesClient().registerInvitationListener(this);

        if (getInvitationId() != null) {
            acceptInviteToRoom(getInvitationId());
            return;
        }

        mListener.onSignInSucceeded();
    }

    // Accept the given invitation.
    void acceptInviteToRoom(String invId) {
        // accept the invitation
        Log.d("mab", "Accepting invitation: " + invId);
        keepScreenOn();

        RoomConfig.Builder roomConfigBuilder = RoomConfig.builder(this);
        roomConfigBuilder.setInvitationIdToAccept(invId)
        .setMessageReceivedListener(this)
        .setRoomStatusUpdateListener(this);
        showWaitScreen();
        getGamesClient().joinRoom(roomConfigBuilder.build());
    }

    // Sets the flag to keep this screen on. It's recommended to do that during the handshake when setting up a game, because if the screen turns off, the game will be cancelled.
    void keepScreenOn() {
        getActivity().getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
    }

    // Clears the flag that keeps the screen on.
    void stopKeepingScreenOn() {
        getActivity().getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
    }

    public void inviteFriends() {
        // show list of invitable players
        Intent intent = getGamesClient().getSelectPlayersIntent(1, 3);
        showWaitScreen();
        startActivityForResult(intent, RC_SELECT_PLAYERS);
    }

    // Leave the room.
    void leaveRoom() {
        Log.d("mab", "Leaving room.");

        mIsMultiplayer = false;
        stopKeepingScreenOn();
        if (mRoomId != null) {
            getGamesClient().leaveRoom(this, mRoomId);
            mRoomId = null;
            showWaitScreen();
        } else {
            showMainMenu();
        }
    }

    // Show the waiting room UI to track the progress of other players as they enter the
    // room and get connected.
    void showWaitingRoom(Room room) {
        Log.d("mab", "GHFrag.showWaitingRoom()");
        mWaitRoomDismissedFromCode = false;

        int minPlayers = MAX_NUM_PLAYERS;  // This just means the "Start" menu item will never be enabled (waiting room will exit automatically once everyone has made a decision)
        Intent i = getGamesClient().getRealTimeWaitingRoomIntent(room, minPlayers);

        // show waiting room UI
        getActivity().startActivityForResult(i, RC_WAITING_ROOM);
    }

    // Forcibly dismiss the waiting room UI (this is useful, for example, if we realize the
    // game needs to start because someone else is starting to play).
    void dismissWaitingRoom() {
        mWaitRoomDismissedFromCode = true;
        getActivity().finishActivity(RC_WAITING_ROOM);  //getActivity() ?????
    }

    // Show error message about game being cancelled and return to main screen.
    void showGameError(String msg) {
        showAlert("Error", "Game Error: " + msg);
        showMainMenu();
    }

    private void showMainMenu() {
        mListener.showMainMenu();
    }

    private void showWaitScreen() {
        mListener.showWaitScreen();
    }

}

MainActivity.java

public class MainActivity extends FragmentActivity implements MainMenuFragment.Listener, PlayFragment.Listener, GameHelperFragmentListener, AlertDialogFragmentListener {

    public static final String MAIN_MENU_FRAGMENT = "MainMenuFragment";
    public static final String PLAY_FRAGMENT = "PlayFragment";
    public static final String WAIT_FRAGMENT = "WaitFragment";


    // Fragments
    MainMenuFragment mMainMenuFragment;
    PlayFragment mPlayFragment;
    WaitFragment mWaitFragment;
    GameHelperFragment gameHelperFragment = null;

    String mIncomingInvitationId = null;

    @SuppressLint("NewApi")
    @Override
    public void onCreate(Bundle savedInstanceState) {
        Log.d("mab", "MainActivity.onCreate()");
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // Add Headless Fragment (if not already retained)
        gameHelperFragment = (GameHelperFragment) getSupportFragmentManager().findFragmentByTag("GameHelperFragment"); 
        if (gameHelperFragment == null) {
            Log.d("mab", this + ": Existing fragment not found.!!!");
            gameHelperFragment = new GameHelperFragment();
            gameHelperFragment.setSignInMessages("Signing in with Google", "Signing out");
            getSupportFragmentManager().beginTransaction().add(gameHelperFragment, "GameHelperFragment").commit();
        } else {
            Log.d("mab", this + ": Existing fragment found.!!!");
        }
    }

    @Override
    public void onSignInFailed() {
        Log.d("mab", "MainActivity.onSignInFailed()");

        if (mMainMenuFragment != null) {
            mMainMenuFragment.updateUi();
        }
    }

    @Override
    public void onSignInSucceeded() {
        Log.d("mab", "MainActivity.onSignInSuccedded()");

        if (mMainMenuFragment != null) {
            mMainMenuFragment.updateUi();
        }
    }

    @Override
    public void onSignInButtonClicked() {
        Log.d("mab", "MainActivity.onSignInButtonClicked()");
        // start the sign-in flow
        beginUserInitiatedSignIn();
    }

    @Override
    public void onSignOutButtonClicked() {
        Log.d("mab", "MainActivity.onSignOutButtonClicked()");
        signOut();

        if (mMainMenuFragment != null) {
            mMainMenuFragment.updateUi();
        }
    }

    @Override
    public void onInvitationReceived(Invitation invitation) {
        mIncomingInvitationId = invitation.getInvitationId();

        // show accept/decline dialog box here.
        String dispName = invitation.getInviter().getDisplayName();
        DialogFragment alertInvitationReceived = AlertDialogFragment.newInstance("Invitation Received", dispName + 
                " is inviting you to play Yahtzee Blast.", "Accept", "Decline", null);
        alertInvitationReceived.show(getSupportFragmentManager(), DLG_INVITATION_RECVD);

    }

    @Override
    protected void onPause() {
        Log.d("mab", "MainActivity.onPause()");
        super.onPause();
    }

    @Override
    protected void onStop() {
        Log.d("mab", "MainActivity.onStop()");
        super.onStop();
    }

    @Override
    protected void onStart() {
        Log.d("mab", "MainActivity.onStart()");
        super.onStart();
    }


    @Override
    protected void onResume() {
        Log.d("mab", "MainActivity.onResume()");
        super.onResume();
    }

    @Override
    protected void onDestroy() {
        Log.d("mab", "MainActivity.onDestroy()");
        super.onDestroy();
        mHelper = null;
    }


    @Override
    protected void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        outState.putString("mIncomingInvitationId", mIncomingInvitationId);  // ? need this ?
    }

    @Override
    public void onInviteFriendsClicked() {
        Log.d("mab", "MainActivity.onInviteFriendsClicked()");
        gameHelperFragment.inviteFriends();
    }

    @Override
    public void onSeeAllInvitationsClicked() {
        Log.d("mab", "MainActivity.onSeeAllInvitationsClicked()");
        gameHelperFragment.seeAllInvitations();
    }

    @Override
    public void onActivityResult(int requestCode, int responseCode, Intent intent) {
        Log.d("mab", this + ": onActivityResult(requestCode: " + requestCode + ", responseCode: " + responseCode + ")");
        super.onActivityResult(requestCode, responseCode, intent);

        // Call GameHelper's onActivityResult in case this result pertains to it
        gameHelperFragment.onActivityResult(requestCode, responseCode, intent);
    }

    public void onAlertDialogFragmentPositiveClicked(String tag) {
        Log.d("mab", "MainActivity.onAlertDialogFragmentPositiveClicked(" + tag + ")");
        if (tag == DLG_INVITATION_RECVD) {
            gameHelperFragment.acceptInviteToRoom(mIncomingInvitationId);
        }
    }

    // Called when we receive a real-time message from the network.
    public void handleRealTimeMessage(RealTimeMessage rtm) {
        Log.d(TAG, "MainActivity.onRealTimeMessageReceived()");
        // Handle it here...
    }

    // Headless Fragment Functions
    private void setSignInMessages(String signingInMessage, String signingOutMessage) {
        gameHelperFragment.setSignInMessages(signingInMessage, signingOutMessage);
    }

    private GamesClient getGamesClient() {
        return gameHelperFragment.getGamesClient();
    }

    private String getInvitationId() {
        return gameHelperFragment.getInvitationId();
    }

    private void beginUserInitiatedSignIn() {
        gameHelperFragment.beginUserInitiatedSignIn();
    }

    private void signOut() {
        gameHelperFragment.signOut();
    }

    private void showAlert(String message) {
        gameHelperFragment.showAlert(message);
    }

    private void showAlert(String title, String message) {
        gameHelperFragment.showAlert(title, message);
    }

    public GameHelperFragment getGameHelperFragment() {
        return gameHelperFragment;
    }

    @Override
    public void showMainMenu() {
        switchToFragment(MAIN_MENU_FRAGMENT, false);
    }

    @Override
    public void showWaitScreen() {
        switchToFragment(WAIT_FRAGMENT, false);
    }

    @Override
    public void participantLeftAtIdx(int idx) {
        // Handle here, if there's anything you need to do.
    }

}

I'm working on an app which has just one activity (which extends BaseGameActivity), and switch between multiple fragments (much like Google's sample code states).

I'm testing a multi-player game right now, on 2 separate devices. Both users can successfully log-in, send messages to each other, etc. However, the instant one user rotates their device, they get kicked out of the room.

I think this makes sense because the activity is getting destroyed and recreated. But what I don't understand is what do we need to do to allow the user to rotate their device and KEEP the game state (logged in, joined to a room, etc) in tact?

  • One thought: android:configChanged="orientation|screenSize" - But Android discourages that (for good reasons, in most cases) - but is this the way we have to go with Google Play Game Services to stay in a room on device orientation change?

  • What about using "onRetainNonConfigurationInstance()" to save the GameHelper instance, and use it again when the activity is recreated?

  • Or somehow implement the game connection (sign-in, room joining, etc) in a Service?

Or am I thinking about this all the wrong way?! Thanks for your thoughts & help. Code example(s) would also be much appreciated if possible.

解决方案

Thank you to @Sheldon for pointing me in the right direction regarding setRetainInstance(true) on a 'headless' fragment. That's the route I took in solving this problem, and now I'd like to paste my code here to hopefully help others. But first:

Verbal Explanation

As stated in the Question, a device orientation change will destroy the MainActivity extends BaseGameActivity, and with it your game state (ie. your connection to Google Play Services). However, we can put all our GameHelper code into a 'headless' Fragment (a fragment without a UI), with setRetainInstance(true) declared. Now, when our MainActivity extends FragmentActivity is destroyed on an orientation change, the headless fragment gets stopped, and even detached, but not destroyed! (onDestroy() is not called) When MainActivity is re-created by Android, our headless fragment gets re-attached to it automatically. At this time, in our headless fragment, onCreate() is NOT called. So onCreate() is the place we connect to GameHelper. We can disconnect from GameHelper in onDestroy() because this will never get called, except when the Application finishes (which, at that time, it's ok to kill our connection).

Note: I think GameHeaderFragment.java should probably be broken up into an Abstract class and a game-specific class which inherits from it (but I didn't do that here).

Here's what I came up with (please forgive the areas where my game-specific code interweaves):

GameHeaderFragment.java

public class GameHelperFragment extends Fragment implements GameHelperListener, OnInvitationReceivedListener, RoomUpdateListener, RoomStatusUpdateListener, RealTimeMessageReceivedListener {

    protected MainActivity mActivity = null;

    // The game helper object. This class is mainly a wrapper around this object.
    protected GameHelper mHelper;

    final static int MAX_NUM_PLAYERS = 4;

    // Request codes for the UIs that we show with startActivityForResult:
    final static int RC_SELECT_PLAYERS = 10000;
    final static int RC_INVITATION_INBOX = 10001;
    final static int RC_WAITING_ROOM = 10002;

    // We expose these constants here because we don't want users of this class
    // to have to know about GameHelper at all.
    public static final int CLIENT_GAMES = GameHelper.CLIENT_GAMES;
    public static final int CLIENT_APPSTATE = GameHelper.CLIENT_APPSTATE;
    public static final int CLIENT_PLUS = GameHelper.CLIENT_PLUS;
    public static final int CLIENT_ALL = GameHelper.CLIENT_ALL;

    // Requested clients. By default, that's just the games client.
    protected int mRequestedClients = CLIENT_GAMES;

    protected String mSigningInMessage = "Signing in with Google";
    protected String mSigningOutMessage = "Signing out";

    // Custom Members
    String mMyId = "";
    String mRoomId = "";
    ArrayList<Participant> mParticipants = null;

    int mCurrentlyPlayingIdx = 0;  // idx into mParticipants
    boolean mIsMultiplayer = false;
    boolean mWaitRoomDismissedFromCode = false;

    public interface GameHelperFragmentListener {
        void onSignInFailed();
        void onSignInSucceeded();
        void onInvitationReceived(Invitation invitation);
        void showMainMenu();
        void showWaitScreen();
        void startGame();
        void participantLeftAtIdx(int idx);
        void handleRealTimeMessage(RealTimeMessage rtm);
    }

    GameHelperFragmentListener mListener;

    public GameHelperFragment() {
        super();
        Log.d("mab", "GHFrag.Constructor()");
    }

    /**
     * Sets the requested clients. The preferred way to set the requested clients is
     * via the constructor, but this method is available if for some reason your code
     * cannot do this in the constructor. This must be called before onCreate in order to
     * have any effect. If called after onCreate, this method is a no-op.
     *
     * @param requestedClients A combination of the flags CLIENT_GAMES, CLIENT_PLUS
     *         and CLIENT_APPSTATE, or CLIENT_ALL to request all available clients.
     */
    protected void setRequestedClients(int requestedClients) {
        mRequestedClients = requestedClients;
    }

    @Override
    public void onAttach(Activity activity) {
        Log.d("mab", this + ": onAttach(" + activity + ")");
        super.onAttach(activity);
        mActivity = (MainActivity) activity;
        mListener = (GameHelperFragmentListener) activity;
    }

    @Override
    public void onCreate(Bundle b) {
        Log.d("mab", this + ": onCreate()");
        super.onCreate(b);
        setRetainInstance(true);
        mHelper = new GameHelper(mActivity);
        mHelper.setup(this, mRequestedClients);  //'this' => GameHelperListener

        mHelper.setSigningInMessage(mSigningInMessage);
        mHelper.setSigningOutMessage(mSigningOutMessage);
        mHelper.onStart(mActivity);

    }


    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        return null;  // Headless Fragment
    }

    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        Log.d("mab", this + ": onActivityCreated()");
        super.onActivityCreated(savedInstanceState);
    }

    @Override
    public void onDestroy() {
        Log.d("mab", this + ": onDestroy()");
        super.onDestroy();
        mHelper.onStop();
    }

    @Override
    public void onActivityResult(int requestCode, int responseCode, Intent data) {
        Log.d("mab", this + ": onActivityResult(" + requestCode + ")");
        super.onActivityResult(requestCode, responseCode, data);
        mHelper.onActivityResult(requestCode, responseCode, data);

        switch (requestCode) {
        case RC_SELECT_PLAYERS:
            // we got the result from the "select players" UI -- ready to create the room
            handleSelectPlayersResult(responseCode, data);
            break;
        case RC_INVITATION_INBOX:
            // we got the result from the "select invitation" UI (invitation inbox). We're
            // ready to accept the selected invitation:
            handleInvitationInboxResult(responseCode, data);
            break;
        case RC_WAITING_ROOM:
            // ignore result if we dismissed the waiting room from code:
            if (mWaitRoomDismissedFromCode) break;

            // we got the result from the "waiting room" UI.
            if (responseCode == Activity.RESULT_OK) {

            } else if (responseCode == GamesActivityResultCodes.RESULT_LEFT_ROOM) {
                // player actively indicated that they want to leave the room
                leaveRoom();
            } else if (responseCode == Activity.RESULT_CANCELED) {
                leaveRoom();
            }

            break;
        }
    }

    // Handle the result of the "Select players UI" we launched when the user clicked the
    // "Invite friends" button. We react by creating a room with those players.
    private void handleSelectPlayersResult(int responseCode, Intent data) {
        if (responseCode != Activity.RESULT_OK) {
            Log.w("mab", "*** select players UI cancelled, " + responseCode);
            showMainMenu();
            return;
        }

        Log.d("mab", "Select players UI succeeded.");

        // get the invitee list
        final ArrayList<String> invitees = data.getStringArrayListExtra(GamesClient.EXTRA_PLAYERS);
        Log.d("mab", "Invitee count: " + invitees.size());

        // get the automatch criteria
        Bundle autoMatchCriteria = null;
        int minAutoMatchPlayers = data.getIntExtra(GamesClient.EXTRA_MIN_AUTOMATCH_PLAYERS, 0);
        int maxAutoMatchPlayers = data.getIntExtra(GamesClient.EXTRA_MAX_AUTOMATCH_PLAYERS, 0);
        if (minAutoMatchPlayers > 0 || maxAutoMatchPlayers > 0) {
            autoMatchCriteria = RoomConfig.createAutoMatchCriteria(
                    minAutoMatchPlayers, maxAutoMatchPlayers, 0);
            Log.d("mab", "Automatch criteria: " + autoMatchCriteria);
        }

        // create the room
        Log.d("mab", "Creating room...");
        RoomConfig.Builder rtmConfigBuilder = RoomConfig.builder(this);
        rtmConfigBuilder.addPlayersToInvite(invitees);
        rtmConfigBuilder.setMessageReceivedListener(this);
        rtmConfigBuilder.setRoomStatusUpdateListener(this);
        if (autoMatchCriteria != null) {
            rtmConfigBuilder.setAutoMatchCriteria(autoMatchCriteria);
        }
        showWaitScreen();

        keepScreenOn();
        getGamesClient().createRoom(rtmConfigBuilder.build());
        Log.d("mab", "Room configured, waiting for it to be created...");
    }

    // Handle the result of the invitation inbox UI, where the player can pick an invitation
    // to accept. We react by accepting the selected invitation, if any.
    private void handleInvitationInboxResult(int response, Intent data) {
        if (response != Activity.RESULT_OK) {
            Log.d("mab", "*** invitation inbox UI cancelled, " + response);
            showMainMenu();
            return;
        }

        Log.d("mab", "Invitation inbox UI succeeded.");
        Invitation inv = data.getExtras().getParcelable(GamesClient.EXTRA_INVITATION);

        // accept invitation
        acceptInviteToRoom(inv.getInvitationId());
    }

    protected GamesClient getGamesClient() {
        return mHelper.getGamesClient();
    }

    protected AppStateClient getAppStateClient() {
        return mHelper.getAppStateClient();
    }

    protected PlusClient getPlusClient() {
        return mHelper.getPlusClient();
    }

    protected boolean isSignedIn() {
        return mHelper.isSignedIn();
    }

    protected void beginUserInitiatedSignIn() {
        mHelper.beginUserInitiatedSignIn();
    }

    protected void signOut() {
        mHelper.signOut();
    }

    protected void showAlert(String title, String message) {
        mHelper.showAlert(title, message);
    }

    protected void showAlert(String message) {
        mHelper.showAlert(message);
    }

    protected void enableDebugLog(boolean enabled, String tag) {
        mHelper.enableDebugLog(enabled, tag);
    }

    protected String getInvitationId() {
        return mHelper.getInvitationId();
    }

    protected void reconnectClients(int whichClients) {
        mHelper.reconnectClients(whichClients);
    }

    protected String getScopes() {
        return mHelper.getScopes();
    }

    protected boolean hasSignInError() {
        return mHelper.hasSignInError();
    }

    protected ConnectionResult getSignInError() {
        return mHelper.getSignInError();
    }

    protected void setSignInMessages(String signingInMessage, String signingOutMessage) {
        mSigningInMessage = signingInMessage;
        mSigningOutMessage = signingOutMessage;
    }

    public void setRoomId(String rid) {
        mRoomId = rid;
    }
    public String getRoomId() {
        return mRoomId;
    }

    @Override
    public void onRealTimeMessageReceived(RealTimeMessage rtm) {
        mListener.handleRealTimeMessage(rtm);
    }

    // Called when we are connected to the room. We're not ready to play yet! (maybe not everybody is connected yet).
    @Override
    public void onConnectedToRoom(Room room) {

        Log.d("mab", "onConnectedToRoom.");

        // get room ID, participants and my ID:
        mRoomId = room.getRoomId();
        mParticipants = room.getParticipants();
        mMyId = room.getParticipantId(getGamesClient().getCurrentPlayerId());

        // print out the list of participants (for debug purposes)
        Log.d("mab", "Room ID: " + mRoomId);
        Log.d("mab", "My ID " + mMyId);
        Log.d("mab", "<< CONNECTED TO ROOM>>");
        Log.d("mab", "  Number of Joined Participants: " + getNumJoinedParticipants());
    }

    // Called when we get disconnected from the room. We return to the main screen.
    @Override
    public void onDisconnectedFromRoom(Room room) {
        mIsMultiplayer = false;
        mRoomId = null;
        showGameError("Disconnected from room");
    }


    @Override
    public void onJoinedRoom(int statusCode, Room room) {
        Log.d("mab", "onJoinedRoom(" + statusCode + ")");
        if (room != null) { Log.d("mab", " roomId: " + room.getRoomId()); }
        if (statusCode != GamesClient.STATUS_OK) {
            mIsMultiplayer = false;
            Log.e("mab", "*** Error: onJoinedRoom, status " + statusCode);
            showGameError("Joined room unsuccessfully: " + statusCode);
            return;
        }
        mRoomId = room.getRoomId();

        // show the waiting room UI
        showWaitingRoom(room);
    }

    // Called when we've successfully left the room (this happens a result of voluntarily leaving
    // via a call to leaveRoom(). If we get disconnected, we get onDisconnectedFromRoom()).
    @Override
    public void onLeftRoom(int statusCode, String roomId) {
        // we have left the room; return to main screen.
        Log.d("mab", "onLeftRoom, code " + statusCode);

        mRoomId = null;  //????? right?

        showMainMenu();
    }

    // Called when room is fully connected.
    @Override
    public void onRoomConnected(int statusCode, Room room) {
        Log.d("mab", "onRoomConnected(" + statusCode + ")");
        if (room != null) { Log.d("mab", " roomId: " + room.getRoomId()); }
        if (statusCode != GamesClient.STATUS_OK) {
            mIsMultiplayer = false;
            Log.d("mab", "*** Error: onRoomConnected, status " + statusCode);
            showGameError("Roon connected unsuccessfully: " + statusCode);
            return;
        }
        mRoomId = room.getRoomId();

        mParticipants = room.getParticipants();  // not sure if we need this here again, but shouldn't hurt (or maybe we want this ONLY here)
        mIsMultiplayer = true;

        // Set 1st player to take a turn
        mCurrentlyPlayingIdx = 0;

        // Start Game!
        mListener.startGame();

    }

    // Called when room has been created
    @Override
    public void onRoomCreated(int statusCode, Room room) {
        Log.d("mab", "onRoomCreated(" + statusCode + ")");
        if (room != null) { Log.d("mab", " roomId: " + room.getRoomId()); }
        if (statusCode != GamesClient.STATUS_OK) {
            mIsMultiplayer = false;
            Log.e("mab", "*** Error: onRoomCreated, status " + statusCode);
            showGameError("Room not created successfully: " + statusCode);
            return;
        }
        mRoomId = room.getRoomId();

        // show the waiting room UI
        showWaitingRoom(room);
    }

    // Called when we get an invitation to play a game. We react by showing that to the user.
    @Override
    public void onInvitationReceived(Invitation invitation) {
        Log.d("mab", "ghFrag.onInvitationReceived()");

        mListener.onInvitationReceived(invitation);
    }

    @Override
    public void onSignInFailed() {
        mListener.onSignInFailed();
    }

    @Override
    public void onSignInSucceeded() {
        // install invitation listener so we get notified if we receive an invitation to play a game.
        getGamesClient().registerInvitationListener(this);

        if (getInvitationId() != null) {
            acceptInviteToRoom(getInvitationId());
            return;
        }

        mListener.onSignInSucceeded();
    }

    // Accept the given invitation.
    void acceptInviteToRoom(String invId) {
        // accept the invitation
        Log.d("mab", "Accepting invitation: " + invId);
        keepScreenOn();

        RoomConfig.Builder roomConfigBuilder = RoomConfig.builder(this);
        roomConfigBuilder.setInvitationIdToAccept(invId)
        .setMessageReceivedListener(this)
        .setRoomStatusUpdateListener(this);
        showWaitScreen();
        getGamesClient().joinRoom(roomConfigBuilder.build());
    }

    // Sets the flag to keep this screen on. It's recommended to do that during the handshake when setting up a game, because if the screen turns off, the game will be cancelled.
    void keepScreenOn() {
        getActivity().getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
    }

    // Clears the flag that keeps the screen on.
    void stopKeepingScreenOn() {
        getActivity().getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
    }

    public void inviteFriends() {
        // show list of invitable players
        Intent intent = getGamesClient().getSelectPlayersIntent(1, 3);
        showWaitScreen();
        startActivityForResult(intent, RC_SELECT_PLAYERS);
    }

    // Leave the room.
    void leaveRoom() {
        Log.d("mab", "Leaving room.");

        mIsMultiplayer = false;
        stopKeepingScreenOn();
        if (mRoomId != null) {
            getGamesClient().leaveRoom(this, mRoomId);
            mRoomId = null;
            showWaitScreen();
        } else {
            showMainMenu();
        }
    }

    // Show the waiting room UI to track the progress of other players as they enter the
    // room and get connected.
    void showWaitingRoom(Room room) {
        Log.d("mab", "GHFrag.showWaitingRoom()");
        mWaitRoomDismissedFromCode = false;

        int minPlayers = MAX_NUM_PLAYERS;  // This just means the "Start" menu item will never be enabled (waiting room will exit automatically once everyone has made a decision)
        Intent i = getGamesClient().getRealTimeWaitingRoomIntent(room, minPlayers);

        // show waiting room UI
        getActivity().startActivityForResult(i, RC_WAITING_ROOM);
    }

    // Forcibly dismiss the waiting room UI (this is useful, for example, if we realize the
    // game needs to start because someone else is starting to play).
    void dismissWaitingRoom() {
        mWaitRoomDismissedFromCode = true;
        getActivity().finishActivity(RC_WAITING_ROOM);  //getActivity() ?????
    }

    // Show error message about game being cancelled and return to main screen.
    void showGameError(String msg) {
        showAlert("Error", "Game Error: " + msg);
        showMainMenu();
    }

    private void showMainMenu() {
        mListener.showMainMenu();
    }

    private void showWaitScreen() {
        mListener.showWaitScreen();
    }

}

MainActivity.java

public class MainActivity extends FragmentActivity implements MainMenuFragment.Listener, PlayFragment.Listener, GameHelperFragmentListener, AlertDialogFragmentListener {

    public static final String MAIN_MENU_FRAGMENT = "MainMenuFragment";
    public static final String PLAY_FRAGMENT = "PlayFragment";
    public static final String WAIT_FRAGMENT = "WaitFragment";


    // Fragments
    MainMenuFragment mMainMenuFragment;
    PlayFragment mPlayFragment;
    WaitFragment mWaitFragment;
    GameHelperFragment gameHelperFragment = null;

    String mIncomingInvitationId = null;

    @SuppressLint("NewApi")
    @Override
    public void onCreate(Bundle savedInstanceState) {
        Log.d("mab", "MainActivity.onCreate()");
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // Add Headless Fragment (if not already retained)
        gameHelperFragment = (GameHelperFragment) getSupportFragmentManager().findFragmentByTag("GameHelperFragment"); 
        if (gameHelperFragment == null) {
            Log.d("mab", this + ": Existing fragment not found.!!!");
            gameHelperFragment = new GameHelperFragment();
            gameHelperFragment.setSignInMessages("Signing in with Google", "Signing out");
            getSupportFragmentManager().beginTransaction().add(gameHelperFragment, "GameHelperFragment").commit();
        } else {
            Log.d("mab", this + ": Existing fragment found.!!!");
        }
    }

    @Override
    public void onSignInFailed() {
        Log.d("mab", "MainActivity.onSignInFailed()");

        if (mMainMenuFragment != null) {
            mMainMenuFragment.updateUi();
        }
    }

    @Override
    public void onSignInSucceeded() {
        Log.d("mab", "MainActivity.onSignInSuccedded()");

        if (mMainMenuFragment != null) {
            mMainMenuFragment.updateUi();
        }
    }

    @Override
    public void onSignInButtonClicked() {
        Log.d("mab", "MainActivity.onSignInButtonClicked()");
        // start the sign-in flow
        beginUserInitiatedSignIn();
    }

    @Override
    public void onSignOutButtonClicked() {
        Log.d("mab", "MainActivity.onSignOutButtonClicked()");
        signOut();

        if (mMainMenuFragment != null) {
            mMainMenuFragment.updateUi();
        }
    }

    @Override
    public void onInvitationReceived(Invitation invitation) {
        mIncomingInvitationId = invitation.getInvitationId();

        // show accept/decline dialog box here.
        String dispName = invitation.getInviter().getDisplayName();
        DialogFragment alertInvitationReceived = AlertDialogFragment.newInstance("Invitation Received", dispName + 
                " is inviting you to play Yahtzee Blast.", "Accept", "Decline", null);
        alertInvitationReceived.show(getSupportFragmentManager(), DLG_INVITATION_RECVD);

    }

    @Override
    protected void onPause() {
        Log.d("mab", "MainActivity.onPause()");
        super.onPause();
    }

    @Override
    protected void onStop() {
        Log.d("mab", "MainActivity.onStop()");
        super.onStop();
    }

    @Override
    protected void onStart() {
        Log.d("mab", "MainActivity.onStart()");
        super.onStart();
    }


    @Override
    protected void onResume() {
        Log.d("mab", "MainActivity.onResume()");
        super.onResume();
    }

    @Override
    protected void onDestroy() {
        Log.d("mab", "MainActivity.onDestroy()");
        super.onDestroy();
        mHelper = null;
    }


    @Override
    protected void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        outState.putString("mIncomingInvitationId", mIncomingInvitationId);  // ? need this ?
    }

    @Override
    public void onInviteFriendsClicked() {
        Log.d("mab", "MainActivity.onInviteFriendsClicked()");
        gameHelperFragment.inviteFriends();
    }

    @Override
    public void onSeeAllInvitationsClicked() {
        Log.d("mab", "MainActivity.onSeeAllInvitationsClicked()");
        gameHelperFragment.seeAllInvitations();
    }

    @Override
    public void onActivityResult(int requestCode, int responseCode, Intent intent) {
        Log.d("mab", this + ": onActivityResult(requestCode: " + requestCode + ", responseCode: " + responseCode + ")");
        super.onActivityResult(requestCode, responseCode, intent);

        // Call GameHelper's onActivityResult in case this result pertains to it
        gameHelperFragment.onActivityResult(requestCode, responseCode, intent);
    }

    public void onAlertDialogFragmentPositiveClicked(String tag) {
        Log.d("mab", "MainActivity.onAlertDialogFragmentPositiveClicked(" + tag + ")");
        if (tag == DLG_INVITATION_RECVD) {
            gameHelperFragment.acceptInviteToRoom(mIncomingInvitationId);
        }
    }

    // Called when we receive a real-time message from the network.
    public void handleRealTimeMessage(RealTimeMessage rtm) {
        Log.d(TAG, "MainActivity.onRealTimeMessageReceived()");
        // Handle it here...
    }

    // Headless Fragment Functions
    private void setSignInMessages(String signingInMessage, String signingOutMessage) {
        gameHelperFragment.setSignInMessages(signingInMessage, signingOutMessage);
    }

    private GamesClient getGamesClient() {
        return gameHelperFragment.getGamesClient();
    }

    private String getInvitationId() {
        return gameHelperFragment.getInvitationId();
    }

    private void beginUserInitiatedSignIn() {
        gameHelperFragment.beginUserInitiatedSignIn();
    }

    private void signOut() {
        gameHelperFragment.signOut();
    }

    private void showAlert(String message) {
        gameHelperFragment.showAlert(message);
    }

    private void showAlert(String title, String message) {
        gameHelperFragment.showAlert(title, message);
    }

    public GameHelperFragment getGameHelperFragment() {
        return gameHelperFragment;
    }

    @Override
    public void showMainMenu() {
        switchToFragment(MAIN_MENU_FRAGMENT, false);
    }

    @Override
    public void showWaitScreen() {
        switchToFragment(WAIT_FRAGMENT, false);
    }

    @Override
    public void participantLeftAtIdx(int idx) {
        // Handle here, if there's anything you need to do.
    }

}

这篇关于Google Play游戏服务多层设备的方向更改使用户离开房间的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆