Skip to content

Commit

Permalink
Multiplayer Stroke Serialization
Browse files Browse the repository at this point in the history
new class which provides a methods to Serializes a LinkedList of Strokes into a byte array and deserialize them in order to use the m_Runner.SendReliableData
  • Loading branch information
sbanca committed Dec 6, 2024
1 parent d517cb2 commit 3e996ee
Show file tree
Hide file tree
Showing 8 changed files with 484 additions and 25 deletions.
51 changes: 37 additions & 14 deletions Assets/Scripts/Multiplayer/MultiplayerManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -405,25 +405,48 @@ void OnRemotePlayerJoined(int id, ITransientData<PlayerRigData> playerData)

if (isUserRoomOwner)
{
StartCoroutine(SaveLoadScript.m_Instance.GetLastAutosaveBytes((byte[] autosaveBytes) =>
{
if (autosaveBytes != null)
{
Debug.Log($"Successfully retrieved {autosaveBytes.Length} bytes from the autosave.");
m_Manager.SendLargeDataToPlayer(id, autosaveBytes);
}
else
{
Debug.LogWarning("Failed to retrieve autosave bytes. Proceed to share command history");
HistorySynchronizationManager.m_Instance.StartSyncronizationForUser(id);
}
}));
SendStrokesToPlayer(id);
}
}

async void SendStrokesToPlayer(int id)
{
LinkedList<Stroke> strokes = SketchMemoryScript.m_Instance.GetMemoryList;
const int chunkSize = 100;
List<Stroke> strokeList = strokes.ToList();

for (int i = 0; i < strokeList.Count; i += chunkSize)
{
var chunk = strokeList.Skip(i).Take(chunkSize).ToList();
byte[] strokesData = await MultiplayerStrokeSerialization.SerializeAndCompressMemoryListAsync(chunk);
m_Manager.SendLargeDataToPlayer(id, strokesData);
Debug.Log($"Sent {strokesData.Length} bytes of serialized stroke data (batch {(i / chunkSize) + 1}) to player {id}.");
}
}


void OnLargeDataReceived(byte[] largeData)
{
SaveLoadScript.m_Instance.LoadFromBytes(largeData);
Debug.Log($"Successfully received {largeData.Length} bytes from the autosave.");

DeserializeReceivedStrokes(largeData);
}

async void DeserializeReceivedStrokes(byte[] largeData)
{

// Decompress and deserialize strokes asynchronously
List<Stroke> strokes = await MultiplayerStrokeSerialization.DecompressAndDeserializeMemoryListAsync(largeData);

Debug.Log($"Successfully deserialized {strokes.Count} strokes.");

// Handle the strokes (e.g., add them to the scene or memory)
foreach (var stroke in strokes)
{
BrushStrokeCommand c = new BrushStrokeCommand(stroke);
SketchMemoryScript.m_Instance.PerformAndRecordNetworkCommand(c, true);
}

}

void OnPlayerLeft(int id)
Expand Down
162 changes: 162 additions & 0 deletions Assets/Scripts/Multiplayer/MultiplayerStrokeSerialization.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
// Copyright 2023 The Open Brush Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using UnityEngine;
using TiltBrush;
using System.Threading.Tasks;
using System.Linq;

namespace OpenBrush.Multiplayer
{
public static class MultiplayerStrokeSerialization
{
public static async Task<byte[]> SerializeAndCompressMemoryListAsync(List<Stroke> memoryList)
{
byte[] serializedData = await SerializeMemoryList(memoryList);
return await Compress(serializedData);
}

public static async Task<List<Stroke>> DecompressAndDeserializeMemoryListAsync(byte[] compressedData)
{
byte[] decompressedData = await Decompress(compressedData);
return await DeserializeMemoryList(decompressedData);
}

// Serializes a LinkedList of Strokes into a byte array using SketchWriter.
// We did not event anything new we are using SketchWriter.WriteMemory from TiltBrush.
public static async Task<byte[]> SerializeMemoryList(List<Stroke> strokeList)
{
try
{
var strokeSnapshots = SketchWriter.EnumerateAdjustedSnapshots(strokeList).ToList();
using (var memoryStream = new MemoryStream())
{
SketchWriter.WriteMemory(memoryStream, strokeSnapshots, new GroupIdMapping());
Debug.Log($"Serialization complete. Serialized data size: {memoryStream.Length} bytes.");
return memoryStream.ToArray();
}
}
catch (Exception ex)
{
Debug.LogError($"Error during serialization: {ex.Message}");
throw;
}
}

// Deserializes a byte array into a List of Strokes using SketchWriter.
// We did not event anything new we are using SketchWriter.GetStrokes from TiltBrush.
public static async Task<List<Stroke>> DeserializeMemoryList(byte[] data)
{
try
{
using (var memoryStream = new MemoryStream(data))
{
var oldGroupToNewGroup = new Dictionary<int, int>();
var strokes = SketchWriter.GetStrokes(memoryStream, allowFastPath: true);

if (strokes != null)
{
Debug.Log($"Successfully deserialized {strokes.Count} strokes from network.");
return strokes;
}
else
{
Debug.LogError("Failed to deserialize strokes.");
return null;
}
}
}
catch (Exception ex)
{
Debug.LogError($"Error during deserialization: {ex.Message}");
throw;
}
}

public static Guid[] GetBrushGuidsFromManifest()
{
// List to store brush GUIDs
List<Guid> brushGuids = new List<Guid>();

// Iterate through each unique brush in the manifest
foreach (BrushDescriptor brush in App.Instance.m_Manifest.UniqueBrushes())
{
if (brush != null)
{
// Add the brush GUID to the list
brushGuids.Add(brush.m_Guid);
Debug.Log($"Brush: {brush.name}, GUID: {brush.m_Guid}");
}
else
{
Debug.LogWarning("Encountered a null brush descriptor.");
}
}

return brushGuids.ToArray();
}

// Compresses a byte array using Brotli.
public static async Task<byte[]> Compress(byte[] data)
{
try
{
return await Task.Run(() =>
{
using var outputStream = new MemoryStream();
using var brotliStream = new BrotliStream(outputStream, CompressionMode.Compress, leaveOpen: true);

brotliStream.Write(data, 0, data.Length);
brotliStream.Flush();

Debug.Log($"Compression complete. Compressed data size: {outputStream.Length} bytes.");

return outputStream.ToArray();
});
}
catch (Exception ex)
{
Debug.LogError($"Error during compression: {ex.Message}");
throw;
}
}

// Decompresses a Brotli-compressed byte array.
public static async Task<byte[]> Decompress(byte[] compressedData)
{
try
{
return await Task.Run(() =>
{
using var input = new MemoryStream(compressedData);
using var brotli = new BrotliStream(input, CompressionMode.Decompress);
using var output = new MemoryStream();
brotli.CopyTo(output);
Debug.Log($"Decompression complete. Decompressed data size: {output.Length} bytes.");
return output.ToArray();
});
}
catch (Exception ex)
{
Debug.LogError($"Error during decompression: {ex.Message}");
throw;
}
}

}
}
11 changes: 11 additions & 0 deletions Assets/Scripts/Multiplayer/MultiplayerStrokeSerialization.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 8 additions & 8 deletions Assets/Scripts/Multiplayer/Photon/PhotonManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,21 +26,18 @@
using UnityEditor;
using UnityEngine.SceneManagement;


namespace OpenBrush.Multiplayer
{
public class PhotonManager : IDataConnectionHandler, INetworkRunnerCallbacks
{

private NetworkRunner m_Runner;

private MultiplayerManager m_Manager;

private List<PlayerRef> m_PlayersSpawning;

private PhotonPlayerRig m_LocalPlayer;

private FusionAppSettings m_PhotonAppSettings;

private int sequenceNumber = 0;
public event Action Disconnected;

public ConnectionUserInfo UserInfo { get; set; }
Expand Down Expand Up @@ -150,6 +147,9 @@ public async Task<bool> JoinRoom(RoomCreateData roomCreateData)
};

var result = await m_Runner.StartGame(args);
m_Runner.ReliableDataSendRate = 60;
m_Runner.Config.Network.ReliableDataTransferModes = NetworkConfiguration.ReliableDataTransfers.ClientToClientWithServerProxy;


if (result.Ok)
{
Expand Down Expand Up @@ -316,8 +316,10 @@ public async Task<bool> RpcSyncHistoryPercentage(int id, int exp, int snt)

public void SendLargeDataToPlayer(int playerId, byte[] largeData)
{
sequenceNumber++;
PlayerRef playerRef = PlayerRef.FromEncoded(playerId);
var key = ReliableKey.FromInts(42, 0, 0, 0);
int dataHash = largeData.GetHashCode();
var key = ReliableKey.FromInts(playerId, sequenceNumber, dataHash, 0);
m_Runner.SendReliableDataToPlayer(playerRef, key, largeData);
}

Expand Down Expand Up @@ -492,8 +494,6 @@ public void OnSessionListUpdated(NetworkRunner runner, List<SessionInfo> session
public void OnReliableDataReceived(NetworkRunner runner, PlayerRef player, ReliableKey key, ArraySegment<byte> data)
{

Debug.Log("Server received reliable data");

byte[] receivedData = data.Array;
if (receivedData == null || receivedData.Length == 0)
{
Expand Down
Loading

0 comments on commit 3e996ee

Please sign in to comment.