Google Play 游戏服务多人设备方向更改将用户踢出房间

2023-11-24

我正在开发一个只有一项活动的应用程序(它扩展了基础游戏活动),并在多个片段之间切换(很像 Google 的示例代码状态)。

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

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

  • 一种想法: android:configChanged="orientation|screenSize" - 但 Android 不鼓励这样做(在大多数情况下,出于充分的理由) - 但这是我们必须使用 Google Play 游戏服务才能在设备方向更改时留在房间中的方式吗?

  • 如何使用“onRetainNonConfigurationInstance()”保存 GameHelper 实例,并在重新创建 Activity 时再次使用它?

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

还是我以错误的方式思考这个问题?感谢您的想法和帮助。如果可能的话,代码示例也将不胜感激。


感谢@Sheldon 为我指明了正确的方向setRetainInstance(true)在“无头”片段上。这就是我解决这个问题的路线,现在我想将我的代码粘贴到这里,希望对其他人有帮助。但首先:

口头解释

正如问题中所述,设备方向改变将摧毁MainActivity extends BaseGameActivity,以及您的游戏状态(即您与 Google Play 服务的连接)。但是,我们可以将所有 GameHelper 代码放入“无头”片段(没有 UI 的片段)中,setRetainInstance(true)宣布。现在,当我们的MainActivity extends FragmentActivity在方向改变时被破坏,无头碎片停止,甚至分离,但是没有被摧毁! (onDestroy()没有被调用)当MainActivity由 Android 重新创建,我们的无头片段会自动重新附加到它。此时,在我们的无头碎片中,onCreate()没有被调用。所以onCreate()是我们连接GameHelper的地方。我们可以断开与 GameHelper 的连接onDestroy()因为这永远不会被调用,除非应用完成(此时,可以终止我们的连接)。

注:我认为GameHeaderFragment.java可能应该分解为一个抽象类和一个继承自它的特定于游戏的类(但我在这里没有这样做)。

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

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

}
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Google Play 游戏服务多人设备方向更改将用户踢出房间 的相关文章

随机推荐

  • 斑点跟踪算法

    我正在尝试使用 OpenCV 创建简单的斑点跟踪 我已经使用 findcontours 检测到了斑点 我想给这些斑点一个恒定的 ID 我收集了前一帧和当前帧中的斑点列表 然后我计算了前一帧和当前帧中每个斑点之间的距离 我想知道还需要什么来跟
  • Airflow:如何在非PythonOperator中使用xcom_push和xcom_pull

    我看到很多关于如何使用的例子xcom push and xcom pull与 Airflow 中的 PythonOperators 一起使用 我需要去做xcom pull from a 非Python运算符类 但找不到如何做 任何指针或示例
  • Node.js / npm - 无论如何判断一个包是否是纯 JS?

    我注意到 在尝试使用 npm 安装看似简单的节点包时 例如nerve 一个 微框架 我经常遇到某种形式的依赖痛苦 经过一番挖掘 我勇敢地找到了问题所在bcrypt模块 显然是用 C C 编写的 必须在包管理器下载后进行编译 不幸的是 如果您
  • OAuth 2 中的访问令牌撤销实现

    我使用 OWIN OAuth 2 来实现我的授权服务器提供程序 现在 我想实现令牌撤销 当我的客户端应用程序想要注销时 任何人都可以帮助我并告诉我如何在 OWIN KATANA OAuth 2 中实现令牌撤销 是否有一些好的做法 OAuth
  • 使用 JSCH 设置目录权限 CHMOD

    在Unix中 如何使用JSCH设置目录权限 我想做 drwxrwxrwx Filezilla 说该整数是 775 但 JSCH 未正确设置权限 JSCH 设置权限后 Filezilla 说是 407 这对我有用 sftp chmod Int
  • 函数式语言:现实生活中的例子

    函数式语言可以解决日常业务问题吗 是否有使用函数式语言 最好是发布的测试用例 实施的成功项目 上面列出的有不少现实世界中的函数式编程 从网站 真实世界的主要标准是该程序主要是为了执行某些任务而编写的 而不是主要为了尝试函数式编程
  • 如何实现 MPVolumeView?

    我希望用户能够使用滑块更改系统音量 并且我意识到实现此目的的唯一方法是使用 MPVolumeView 但我找不到它的任何示例代码 并且我尝试实现的每个方法都不会显示 那么实现 MPVolumeView 最简单 正确的工作方法是什么 将其作为
  • 如何以编程方式在四开本中生成选项卡集面板?

    我在下面提供了一个可重复的小示例 我想在四开中为命名列表中的每个 ggplot 对象生成选项卡plots 下面的四开文档将在其自己的二级标题中呈现图形 但不会按预期呈现在选项卡中 title Untitled format html r l
  • 修改innerHTML后保存/恢复内容可编辑的选择

    我知道在 contentEditable 中获取 设置光标位置几乎是不可能的 我不在乎知道这些信息 我需要能够保存当前选择 修改 div 的innerHTML 然后恢复选择 我一直在尝试提供的答案contenteditable 选定的文本保
  • Windows 10 TTS 语音未显示?

    我安装了一些英语语言包 美国 英国和加拿大 及其语音选项 我可以在 Windows 10 设置 gt 语音中访问它们 但它们不会显示在控制面板提供的文本到语音选项中 我无法通过应用程序使用声音 我可以使用默认声音 David 和 Zira
  • 以此比例因子查询耗尽的资源

    我在 Amazon Athena 上运行 SQL 查询 我多次收到以下错误 以此比例因子查询耗尽的资源 此查询针对 test1 数据库运行 除非查询限定 请在我们的论坛上发布错误消息或联系客户支持并提供查询 ID 如果没有看到查询 很难确定
  • 尝试使用 XPages 将多值字段连接到 Java Bean 时出现类型不匹配

    我有这个代码
  • php 中的新限制:每个 POST 1000 个字段。有人知道这个数字是否会受到影响吗?

    在较新的 PHP 版本中 每个公式 POST 的输入字段数将限制为 1000 未经验证的信息 看来这个限制已经安装在 5 2 的某些版本中 这给我们的网上商店带来了很多问题 有人了解更多吗 这个限制是否会受到参数或变量的影响 我刚刚找到了
  • 为什么这个野牛代码会产生意外的输出?

    弹性代码 1 option noyywrap nodefault yylineno case insensitive 2 3 include stdio h 4 include tp tab h 5 6 7 8 return 9 retur
  • 如何对互联网地址进行编码

    发送电子邮件的代码如下 MimeMessage msg new MimeMessage session msg setSubject subject UTF 8 here you specify your subject encoding
  • 如何向 SqlParameter[ ] 集合添加新参数?

    我想使用类似的东西 using DataSet ds new DataSet SqlParameter dbParams new SqlParameter new SqlParameter PromptID promptID if scen
  • 如何为 alamofire Post 请求中的参数之一传递 nil 值

    我想传递一个 nil 值 即可选参数值之一 并且它必须以 nil 值继续阿拉莫菲尔 帖子请求 如果您告诉我下一步如何进行 会有帮助吗 let image UIImage UIImage let imageData UIImagePNGRep
  • 在 Perl 中,当我在同一个哈希上循环时,从哈希引用中删除键是否安全?为什么?

    我基本上想这样做 foreach my key keys hash ref Do stuff with my key and hash ref Delete the key from the hash delete hash ref gt
  • 为什么链接中的目标属性有下划线?

    我已经编码很多年了 直到此刻我才意识到这个属性target从元素 a 要求它们的所有值都以下划线开头 我们很多人都知道 但我不知道为什么 我的意思是 从我的头脑中 我不记得除此之外的另一个需要以下划线开头的值 有谁知道这个实现背后的原因是什
  • Google Play 游戏服务多人设备方向更改将用户踢出房间

    我正在开发一个只有一项活动的应用程序 它扩展了基础游戏活动 并在多个片段之间切换 很像 Google 的示例代码状态 我现在正在两台独立的设备上测试多人游戏 两个用户都可以成功登录 互相发送消息等 但是 一旦一个用户旋转设备 他们就会被踢出