Google Play Game Services Multi-Player Device Orientation change kicks user out of room
Asked Answered
B

3

9

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.

Bout answered 11/7, 2013 at 13:0 Comment(1)
Android manifest, sigleTop=true. Essentially you need to make your app a singleton and to always use one instance. Give it a try.Morph
B
10

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

}
Bout answered 17/7, 2013 at 12:31 Comment(0)
M
1

Here's an idea. But before that, a bit of illustration.

Android apps can be killed by Android Resource Manager at any time due to memory or whatever reasons it decides. So, to retain a "always on" permanent deamon, we use services.

A service would be neat to have here because your app can communicate it's state to the service, which in turn holds all the real data (is connected, connected to which server, server connection etc) and just reconnects to the service.

Having this service will add an aditional benefit of potentially telling you that your remote client has disconnected (if the service isn't bound to an app, the user def disconnected) and could help with fine-graining connectivity, as the service mediates between your server and the GUI client. For all intents and purposes, the service is the real client that plays the game, and is just being driven by a gui client which tells the service what it should try to do. This way the service is seemless to the user and his playstate is always retained.

But first I'd try to make my app a singleton, via AndroidManifest to only use one instance of the application (singleTop) or to always use the same process (sameProcess but unsure if that even helps tiny bit).

Then if that fails, I'd take good, less painful routes, until finally I see that the service is the way to go. So maybe there's a lightweight fix for your issue, maybe you just need a simple deamon service and call it a day.

Morph answered 11/7, 2013 at 13:54 Comment(0)
S
1

I have encountered exactly the same issue and I am currently working on fixing this.

Initially I had just implemented using

    android:configChanges="keyboardHidden|orientation|screenSize"
    android:screenOrientation="portrait"

in the manifest because neither my game nor my fragment based implementation of the Game Services stuff correctly supported orientation changes. i.e. I am confident that the shortcomings are in my own code rather than the Google supplied stuff.

In my case, the users are kicked out of the game because when one or other of the players "rotate", I do not restart my fragments correctly. I then receive the onLeftRoom callback and I choose to finish at this point.

I am taking the opportunity to improve and simplify my own fragment based implementation of the Game Services stuff and this is my basic plan:

A fragment activity (ABS) that comprises:

  • Some simple UI fragment tabs for "Quick Game", "Leaderboards" etc.

  • A "headless" (no UI) setRetainInstance(true) fragment that is my equivalent of the "BaseGameActivity" sample and which performs all the calls to GameHelper.

  • GameHelper - unchanged supplied version.

The advantage of this approach is that by using the "headless" fragment I should avoid some of the tricky problems that I see at the moment. e.g. even after restarting some fragments I still get getActivity() null errors and I find myself trying to solve problems that seem far too complicated for such a simple little game .

By the way, I would not be happy if any game (least of all a dumb one like mine) was running as a service on my phone / tab - but that's just my opinion. I think that a setRetainInstance(true) fragment which dies with its parent activity is perfectly adequate.

I'm interested to hear anybody elses thoughts on this.

For those that are not aware (source http://www.vogella.com/articles/AndroidFragments/article.html#headlessfragments1):

8.2. Retained headless fragments to handle configuration changes Headless fragment are typically used to encapsulate some state across configuration changes or for a background processing task. For this purpose you would set your headless fragment to be retained. A retained fragment is not destroyed during configuration changes.

Stralka answered 12/7, 2013 at 0:0 Comment(4)
thanks for your thoughts. At first I thought your idea might work (the 'headless' fragment), but then thought more about it, and I 'think' it won't - for this reason: assuming we're NOT using 'android:configChanges="orientation"' and our MainActivity contains all our game logic, and contains the headless fragment - when the user rotates the screen, MainActivity will be destroyed (and re-created). When it's destroyed, the headless fragment goes with it (and we're back where we started). Hence, the initial thought that maybe a Service is needed (something that sticks around).Bout
No ! Here is a good reference on fragments. vogella.com/articles/AndroidFragments/article.html If you read this and research thoroughly on Stack Overflow you will realise that a setRetainInstance(true) fragment is not destroyed on a configuration change.Stralka
Hmm, good stuff. Thanks for the link. Got the headless fragment to work (but very untested right now) for the GameHelper. Will post some code when I get a chance & after more testingBout
Good, thanks. I've written it, but haven't tried rotations yet.Stralka

© 2022 - 2024 — McMap. All rights reserved.