Search Unity

Question Timestamp Compression for Network-serialized Data

Discussion in 'NetCode for ECS' started by devook, Oct 17, 2023.

  1. devook

    devook

    Joined:
    May 21, 2013
    Posts:
    30
    I am building a multiplayer ARPG where it is critically important that combat animations - which inform whether, how, and when damage events happen - are as in-sync as possible across all client machines. To that end, I feel that I need a Timestamp attached to every entry in my character actions buffer to inform non-owning clients on how far in the past an animation started in the owner's simulation. However, I don't want to send a double over the network, and in general it feels unnecessary to send the entire game time as a timestamp as that can become a very large number over a multiple-hour play session.

    I've devised a compression strategy that I think will work, where I measure time as a delta from a "rolling epoch" that clamps to every integer value of
    (Time * 1/60) / UInt8.MaxValue
    . My question is: do I actually need to implement this myself? It seems like timestamp compression would be a required feature for more than just my specific game, but I haven't found anything on the forums or in the documentation on an existing implementation, am I just missing it?
     
  2. devook

    devook

    Joined:
    May 21, 2013
    Posts:
    30
    Ok after digging a little more into the docs, it seems like NetworkTick attempts to solve this problem, with the added benefit of accommodating for packet travel time, albeit using more bytes than I'd like... So I guess my question could be amended to: How can I convert a NetworkTick into elapsed server time?

    When I receive an ability from my action buffer on a non-owning client, I need to be able to measure the difference between the current NetworkTick and the one attached to my action struct in game time in order to know where to set the start point in my ability Playable

    Edit: Slightly more digging has uncovered the NetworkTickRate struct so I might need to again amend my question to: Which TickRate describes the interval between subsequent NetworkTick values? If
    serverTickA -  serverTickB = 1
    , how much time has elapsed? Is it
    1 / SimulationTickRate
    or
    1 / NetworkTickRate
    ? From the naming I would assume the latter, however, the manual for Prediction seems to strongly imply that the Server runs the PredictionLoop (at SimulationTickRate) against the value of ServerTick, which would imply the ServerTick is incremented every simulation tick...
     
    Last edited: Oct 17, 2023
  3. NikiWalker

    NikiWalker

    Unity Technologies

    Joined:
    May 18, 2021
    Posts:
    316
    Hey devook,

    Are these PvP actions, or PvE, or PvPvE? You have to pay for latency somewhere, which means you have a fairly large upfront choice:
    • You either Client Predict all combatants (including other human players, as well as NPC i.e. bot-controlled enemies), which means that you'll very likely have to deal with mispredictions (this depends heavily on the pace of the game). This is similar to the GGPO style, although we're not fully deterministic, so we still synchronise state in Netcode for Entities. If animation times are long (100ms+) and non-cancellable, I can see this working relatively well.

      But note, it is impossible to have this look 100% accurate. Client predictions are just that - predictions. If your attack can be invalidated by an enemy attack, for example, clients will incorrectly predict very frequently.

    • OR you pay the latency cost up front, by treating all clients (including your own) as Interpolated. Thus, all input actions will have noticeable delay. This is more of an old-school RTS style (again though, not using deterministic lockstep), but will guarantee that WYSIWYG, as everyone will be viewing the unified resulting timeline (via Eventual Consistency). This sounds unworkable for your use-case.
    In either case, there is likely no need to replicate timers, as there are two timelines, and the above should comfortably put you on both.

    This sounds like a hybrid third model, where other players are "semi-predicted". I think you'd be better off simply making them entirely predicted, at that point.

    To predict everyone, see our Prediction Switching sample here: https://github.com/Unity-Technologi...odeSamples/Assets/Samples/PredictionSwitching
    Note that the Player prefab defaults to Interpolated, but then any ghosts which come within X units of your client's ghost position will be automatically switched to Predicted (including your own player). Enable the Bounding Box Viewer on the Multiplayer > PlayMode Tools to visualize this.

    You'll also notice that the input struct for this sample - PredictionSwitchingInput - has [GhostField]'s on the inputs, as well as [GhostComponent(OwnerSendType = SendToOwnerType.SendToNonOwner)]. This allows them to be sent to other clients (improving prediction).

    Note: If you choose the "interpolate everything" route, note that AutoCommandTarget doesn't currently work with Interpolated ghosts (even if their ownership is correctly setup).
     
  4. devook

    devook

    Joined:
    May 21, 2013
    Posts:
    30
    While I'm not ruling out PvP, my primary focus is cooperative PvE right now, and I would like to support at least 8 people in a multiplayer session (and preferably more, hoping I can use my prototype to test upper bounds...) I'm working on a Souls-like, so animations are likely in the range of >500ms up to several seconds, but will be cancellable and interruptible. Prediction on non-owner clients feels like it would be too much re-simulation with so many players, but I would like to split the difference by letting non-owned player animations "catch up" to the current time on a given client's machine. In a sense I think I need to implement my own minimalist version of a rollback system specifically for handling attack animations that does not forward simulate over each tick that's occurred since the start of the animation, but simply re-evaluates the state in the present tick assuming an animation started some N ms in the past with some initial transform T.

    But I may be overthinking it and should probably start with Prediction on all clients and see how far that gets me!
     
  5. NikiWalker

    NikiWalker

    Unity Technologies

    Joined:
    May 18, 2021
    Posts:
    316
    Ah sorry, to clarify: While you would need the ability to predict nearby enemies and other players, you can still set a range limit (or similar) on Prediction itself. The Prediction Switching sample shows off said mechanism (called "Prediction Switching") for switching a ghost between predicted and interpolated at runtime, based on its radius to your clients character controller (or camera).
     
  6. CMarastoni

    CMarastoni

    Unity Technologies

    Joined:
    Mar 18, 2020
    Posts:
    896
    First of all, being the server simulation "tick" based, using NetworkTick as a measure of the elapsed time it is most correct and simple things to do.
    As you already discovered, DeltaTime = (TickB-TickA)/SimulationTickRate.

    Predicting all the players and the enemy can be indeed costly, but unless you have hundred of them in close combat , you should be good in term of CPU costs. I don't think you are going to be CPU bound for this, but indeed it add cost, that can be used instead for other things.

    As Niki suggested, using prediction switching is the way to go in this case for two reason:
    - Far players ad enemy will look just nice and fine
    - Close combat will then work and be reactive.

    In term of what can make the prediction loop slow, I think one thing can be the interaction of animation with the simulation and the fact you may want to use root motion as well.
    That would imply, the animation should run as part of the prediction group (ticked manually) and that in turn may prevent to use burst. I would strongly suggest to avoid using Animator for that (in my experience with souls-like style game) and focusing on Playable, that give some better options (and scalability and speed).

    The real problem in general would be the interaction with the players and what you expect from it. Even with prediction, you still need to account the fact that
    - Snapshot can be lost
    - Ability activation can be hardly predicted, unless ability auto-kick in. If they are activated by user actions, you will anyway end-up noticing that only when the server will send you the snapshot that contains that information.

    By using remote player input prediction (input sent to all players) with the default settings, you only a couple of tick (usually the slack, so by default 2) of future player input you can exploit to trigger abilities for the remote players, but it is just a very little margin. Also, if some snapshot are lost, you may still end-up losing the player action trigger at the right tick.

    However, if the activation of an ability is masked by some animation on the client, so the ability start X frame in the future after a key press for example, this give another margin to make it possible to have some anticipatory action on the remote players.
     
  7. devook

    devook

    Joined:
    May 21, 2013
    Posts:
    30
    Right now my abilities are authored as Timeline assets, with a utility MonoBehavior called the AbilityExecutor managing each character's PlayableDirector: switching Timelines when appropriate and setting/unsetting track bindings, etc. This all still ultimately goes through an AnimatorController, though... Is there some way I can apply an AnimationClip to a given character's avatar without it?


    Yes this is exactly what I'm worried about when attempting to work purely off prediction. If I lose the snapshot where the action occurred, I can't know when the ability started for non-owning clients. However I'm struggling to understand how I would switch between the two, as it seems like I would need to have different attributes declared on my custom ComponentData when predicting vs. when interpolating. When predicting, I'd want to send the input data to each client so they can interpret it locally, and compute their own values for the component I'm calling AbilityExecutionState (which is evaluated every tick and applied to the player's GameObject). So, when predicting, AbilityExecutionState is not a Ghost Component, and every client is responsible for simulating its state explicitly. However, if I'm interpolating, I would want to send AbilityExecutionState from the Server to interpolated clients and have local clients evaluate AbilityExecutionState without computing it themselves. So, when interpolating, AbilityExecutionState is a Ghost Component. Were I to try and implement this strategy as Niki described, how do I deal with a Component that switches between being and not being Ghost data at runtime, when that's a compile-time declaration? Do I need to split AbilityExecutionState into two separate, nearly identical Components like InterpolatedAbilityState and PredictedAbilityState that I dynamically add/remove from entities?


    Also... feels like maybe I should start a bug thread for this, but I'm not totally sure I understand what the expected behavior is when using a DynamicBuffer of ICommandData. What I've been attempting (and failing) to implement today is a mechanism by which the PlayerInputSystem, which samples inputs and populates my InputComponentData, also adds Attack commands as CommandData to a DynamicBuffer attached to each player Entity. However, I seem to be misunderstanding how CommandData works, because it seems like even though I explicitly set the Tick field before putting it in the buffer, when my MovementAndAbilitySystem processes the buffer and subsequently removes the CommandData, it just comes back next frame with an incremented Tick value. This happens every time I remove the data from the buffer for the next ~30 or so ticks.

    upload_2023-10-20_15-14-17.png
    Despite only ever putting a single instance of my CommandData into the buffer, the next System in the chain detects multiple entires, all with subsequently increasing Tick values...