Replicate brush strokes as self-contained commands instead of texture pixels: the owner emits each committed stamp (local placement plus brush state, byte-quantized) and remotes replay it through the same draw path, reproducing identical maps without streaming megabytes. - MeshPainter.Net.cs: PaintStampNet serializable stamp, OnStampCommitted owner-capture event (no-op without listener), ApplyNetworkStamp remote replay, seedable spray for deterministic speckle. - MeshPainter.cs: emit stamp after drawing, seed spray only when synced. - PlayerPaintSync.cs: NetworkBehaviour batching owner stamps reliably (ServerRpc -> ClientRpc) with echo guard; remote disables local painting. - Player/Hider/Hunter prefabs: PlayerPaintSync plus NGO GlobalObjectIdHash. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
89 lines
3.1 KiB
C#
89 lines
3.1 KiB
C#
using System.Collections.Generic;
|
|
using Unity.Netcode;
|
|
using UnityEngine;
|
|
|
|
/// <summary>
|
|
/// Replicates live mesh painting across the network as brush-stamp commands (not pixels). The owner
|
|
/// captures every committed stamp from its <see cref="MeshPainter"/>, batches the frame's stamps and
|
|
/// forwards them; remote avatars replay each stamp through their own painter onto the identical
|
|
/// mesh/UVs. Position/animation come from the other sync components; this only carries the tiny paint
|
|
/// commands (~tens of bytes each), so two players see each other paint live without streaming the
|
|
/// 12 MB of paint maps.
|
|
///
|
|
/// Live-only milestone: a client that joins AFTER painting happened does not receive the prior strokes
|
|
/// (no snapshot baseline yet) - it starts seeing paint from the moment it connects.
|
|
/// </summary>
|
|
[DisallowMultipleComponent]
|
|
public class PlayerPaintSync : NetworkBehaviour
|
|
{
|
|
[Tooltip("Max brush stamps forwarded to the network per frame. Pathological fast strokes (hundreds " +
|
|
"of interpolated sub-stamps in one frame) are decimated to this to bound the RPC size; local " +
|
|
"painting stays full fidelity.")]
|
|
[SerializeField] private int maxStampsPerFrame = 64;
|
|
|
|
private MeshPainter _painter;
|
|
private readonly List<PaintStampNet> _pending = new List<PaintStampNet>();
|
|
|
|
private void Awake() => _painter = GetComponentInChildren<MeshPainter>(true);
|
|
|
|
public override void OnNetworkSpawn()
|
|
{
|
|
if (!_painter)
|
|
return;
|
|
|
|
if (IsOwner)
|
|
{
|
|
_painter.OnStampCommitted += OnStampCommitted;
|
|
}
|
|
else
|
|
{
|
|
// Remote replica: never paint from this machine's mouse; paint only from replicated stamps.
|
|
_painter.PaintingEnabled = false;
|
|
}
|
|
}
|
|
|
|
public override void OnNetworkDespawn()
|
|
{
|
|
if (_painter && IsOwner)
|
|
_painter.OnStampCommitted -= OnStampCommitted;
|
|
}
|
|
|
|
private void OnStampCommitted(PaintStampNet stamp) => _pending.Add(stamp);
|
|
|
|
private void LateUpdate()
|
|
{
|
|
if (!IsOwner || _pending.Count == 0)
|
|
return;
|
|
|
|
SubmitStampsServerRpc(BuildBatch());
|
|
_pending.Clear();
|
|
}
|
|
|
|
/// <summary>Returns this frame's stamps, uniformly decimated to maxStampsPerFrame if a fast stroke overran it.</summary>
|
|
private PaintStampNet[] BuildBatch()
|
|
{
|
|
int count = _pending.Count;
|
|
if (count <= maxStampsPerFrame)
|
|
return _pending.ToArray();
|
|
|
|
var outp = new PaintStampNet[maxStampsPerFrame];
|
|
for (int i = 0; i < maxStampsPerFrame; i++)
|
|
outp[i] = _pending[(int)((long)i * count / maxStampsPerFrame)];
|
|
return outp;
|
|
}
|
|
|
|
[ServerRpc]
|
|
private void SubmitStampsServerRpc(PaintStampNet[] stamps) => ApplyStampsClientRpc(stamps);
|
|
|
|
[ClientRpc]
|
|
private void ApplyStampsClientRpc(PaintStampNet[] stamps)
|
|
{
|
|
// The owner already painted these locally; only remotes replay them.
|
|
if (IsOwner || !_painter)
|
|
return;
|
|
|
|
for (int i = 0; i < stamps.Length; i++)
|
|
_painter.ApplyNetworkStamp(stamps[i]);
|
|
}
|
|
}
|