⌘K
↑↓ Navigate ↵ Open Esc Close
Tier 4One-Way MatchMatchMakingPlayback Stream21 / 29

One-Way Match: attack while they're offline, replay it when they're back

Async Match: the other connectionless multiplayer pattern in this series; the two solve different problems (turn-taking vs. one-sided raids).

brainCloud calls this pattern One-way Offline: asynchronous multiplayer popularized by Clash of Clans and Boom Beach, where a player attacks an opponent's base while they're offline, and the opponent sees a replay of what happened once they're back. BCClashers (repositories/unity-examples/BCClashers) builds this out of three services working together: MatchMaking finds an opponent and tracks rating, One-Way Match brackets the attack itself, and Playback Stream records every action so the defender can watch exactly what happened, frame by frame, the next time they log in.

The full lifecycle

One-Way Match's whole raid loop is six stages, in a fixed order, spanning three services.

  1. Enable. EnableMatchMaking opts the local player into being a valid target for other players' searches. It doesn't grant the caller anything; it's a visibility switch, off by default, that controls whether this player can be found at all.
  2. Search. FindPlayersUsingFilter(rangeDelta, numMatches, jsonExtraParams, ...) runs an app-defined Cloud Code filter server-side, not a built-in brainCloud rule. This app's filter is named FilterOneWayMultiplayer in its own code comment ("drops candidates with no raidable defense, server-side"); the filter script itself isn't in this repo.
  3. Attack starts. OneWayMatchService.StartMatch brackets a Playback Stream and returns its playbackStreamId. From this point, every action is recorded against that stream id instead of sent live, since the defender isn't connected.
  4. Record. PlaybackStreamService.AddEvent(streamId, eventData, summaryData, ...) logs each meaningful action as it happens. This app adds only one event, for the final outcome (eventId: Defender, the rank achieved), but the same call works for any attack-in-progress action worth replaying later.
  5. Seal, in order. PlaybackStreamService.EndStream closes the recording first, then OneWayMatchService.CompleteMatch(streamId) finalizes the match. The stream must be sealed before the match that owns it is marked complete.
  6. Settle. A win or loss adjusts rating via MatchMakingService. IncrementPlayerRating/DecrementPlayerRating. Separately, and at any time, not just post-match, a defender can call TurnShieldOnFor(minutes, ...) to mark themselves temporarily protected; whether step 2's filter actually excludes a shielded defender depends entirely on what that app-defined filter script checks, since brainCloud itself doesn't enforce shield exclusion automatically.

Code

Two-step opponent search, with a server-side eligibility filterNetworkManager.cs:210-224
public void LookForPlayers()
{
    _bcWrapper.MatchMakingService.EnableMatchMaking(OnEnableMatchMaking, OnFoundPlayersError);
}

private void OnEnableMatchMaking(string jsonResponse, object cbObject)
{
    // FilterOneWayMultiplayer drops candidates with no raidable defense, server-side.
    _bcWrapper.MatchMakingService.FindPlayersUsingFilter(
        _findPlayersRange, _numberOfMatches, CreateJsonMatchFilterParms(),
        OnFoundPlayers, OnFoundPlayersError);
}
Starting the match brackets a playback recordingNetworkManager.cs:743-758
_bcWrapper.OneWayMatchService.StartMatch(opponentId, _findPlayersRange, OnStartMatchSuccess, OnFailureCallback);

private void OnStartMatchSuccess(string in_jsonResponse, object cbObject)
{
    var data = (JsonReader.Deserialize(in_jsonResponse) as Dictionary<string, object>)["data"] as Dictionary<string, object>;
    _playbackStreamId = data["playbackStreamId"] as string;
    GameManager.Instance.LoadToGame();
}
Recording an outcome, then sealing the stream and the match, in orderNetworkManager.cs:715-727
Dictionary<string, object> eventData = new Dictionary<string, object>();
eventData.Add("eventId", (int)EventId.Defender);
eventData.Add("defenderRank", in_defenderRank);
_bcWrapper.PlaybackStreamService.AddEvent(_playbackStreamId, JsonWriter.Serialize(eventData), CreateEndGameSummaryData(), OnRecordSuccess, OnFailureCallback);

private void OnRecordSuccess(string in_jsonResponse, object cbObject)
{
    _bcWrapper.PlaybackStreamService.EndStream(_playbackStreamId);       // seal the recording first
    _bcWrapper.OneWayMatchService.CompleteMatch(_playbackStreamId);      // then finalize the match
}
Replaying any past match by its stream id (NetworkManager.cs:761-780, abridged)
// Used by the "Watch" button on the dashboard's match-history cards.
public void ReadStreamById(string in_playbackStreamId)
{
    _bcWrapper.PlaybackStreamService.ReadStream(in_playbackStreamId, OnReadStreamSuccess, OnFailureCallback);
}
Defensive shield: 60 minutes, not 60 secondsNetworkManager.cs:1038-1044
public void TurnOnShield()
{
    if (_shieldActive) return;
    DecreaseGoldAmountForShield();
    GameManager.Instance.CurrentUserInfo.ShieldTime = 60;
    _bcWrapper.MatchMakingService.TurnShieldOnFor(60, OnTurnOnShieldSuccess);
}

Gotchas worth knowing up front

  • Three services, one feature. MatchMaking (opponent search and rating), One-Way Match (the attack bracket itself), and Playback Stream (the recording) are three separate services. They add up to asynchronous raid multiplayer only when used together; no single service does all three.
  • Nothing in the API enforces stream-then-match ordering; it has to be enforced by the caller. EndStream before CompleteMatch is a convention this app follows carefully. The method signatures don't prevent calling them out of order, but doing so seals a match around an unfinished recording.
  • Re-reading old matches uses the same ReadStream call as watching your own attack. The dashboard's match-history "Watch" button and the code path for viewing your own raid are the same method, fed a different playbackStreamId.
  • TurnShieldOnFor's argument is minutes, not seconds. brainCloud's own reference confirms it, and this app's code agrees with itself throughout: ShieldTime gets set from a server-returned .Minutes elsewhere, and the on-screen countdown timer converts it with ShieldTime * 60 to get seconds. TurnOnShield's TurnShieldOnFor(60, ...) call shields the base for a full hour, not one minute.
  • This is only one SDK's worth of evidence. No other reference app in this repo exercises One-Way Match, MatchMaking's rating/shield calls, or Playback Stream. The pattern isn't yet proven across every client.