Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 8 additions & 7 deletions docs/core/add-to-your-project.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ Install TsVRC Core in a VRChat world project.

## Requirements

- **Unity 2022.3.** The Core package manifest targets this version specifically.
- **Unity 2022.3.** The Core package manifest targets this version.
- **The [VRChat Worlds SDK](https://creators.vrchat.com/worlds/) `3.10.x`.**

## Install the package
Expand All @@ -19,19 +19,20 @@ Install **TsVRC Core** (`com.tsvrc.core`) as described in
[Install a TsVRC package](/docs/install-a-package). Its `.unitypackage` is on
[the Core GitHub releases](https://github.com/tsvrc/tsvrc-core/releases).

Core depends on [Udon Test Kit](/docs/udon-test-kit/intro) (`com.tsvrc.udon-test-kit`) for its
testing helpers. The Creator Companion installs it along with Core. With the `.unitypackage`,
import the [Udon Test Kit](/docs/udon-test-kit/add-to-your-project) one too, unless your project
already has the kit.
Core depends on [Udon Test Kit](/docs/udon-test-kit/intro) (`com.tsvrc.udon-test-kit`): its
codegen uses the kit to [hold back while tests run](./testing/testing-your-world#codegen-during-a-test-run).
The Creator Companion installs it along with Core. With the `.unitypackage`, import the
[Udon Test Kit](/docs/udon-test-kit/add-to-your-project) one too, unless your project already has
the kit.

You'll know it worked when the Unity menu bar gains a **Tsvrc** menu. Everything Core does
from here runs from there.

## What you just added

The Core manifest description sums it up: structured initialization, dependency wiring,
and editor codegen tooling for building VRChat worlds with UdonSharp. Concretely, the
package compiles as one runtime assembly (`Tsvrc.Runtime`), the base classes your world
and editor codegen tooling for building VRChat worlds with UdonSharp. The package compiles as
one runtime assembly (`Tsvrc.Runtime`), the base classes your world
scripts extend and call into, and one editor-only assembly (`Tsvrc.Editor`) holding all
the codegen and Configure-window tooling. See [How Core fits
together](./core-concepts/how-it-fits-together) for how those two halves relate.
Expand Down
252 changes: 100 additions & 152 deletions docs/core/core-concepts/process.md

Large diffs are not rendered by default.

107 changes: 47 additions & 60 deletions docs/core/explanations/sync-model.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,70 +6,57 @@ sidebar_position: 3

# Decision note: the shared-memory and sync model

Every networked Core type that needs to replicate state, from
[`TsvrcMemory`](../networking-data/tsvrc-memory) to [`Process`](../core-concepts/process)
and everything built on it, to [`TsvrcTimer`](../networking-data/tsvrc-timer), uses the
same two ingredients: the VRChat manual sync mode (`BehaviourSyncMode.Manual`) plus an
explicit `RequestSerialization()` call whenever something changes, rather than the VRChat
automatic continuous sync. This note ties together why that combination is the framework's
consistent default, synthesizing what each individual type's own reference page already
establishes rather than introducing new claims.
Every Core type that replicates state, from [`TsvrcMemory`](../networking-data/tsvrc-memory) to
[`Process`](../core-concepts/process) and the types built on it such as
[`TsvrcTimer`](../networking-data/tsvrc-timer), uses the same two pieces: the VRChat manual sync mode
(`BehaviourSyncMode.Manual`) and an explicit `RequestSerialization()` call after each change. This
note explains why.

## Manual sync, sent on demand

[The VRChat documentation of the two sync
modes](https://creators.vrchat.com/worlds/udon/networking/variables/#2-manual-sync) states
that manual sync "requires calling `RequestSerialization()` to send data updates" and is
"best for crucial values that do not change often," while continuous sync "does not require
calling `RequestSerialization()`" and instead "updates automatically" on the VRChat
schedule, better suited to "frequently updated values." Every Core type built on `Process`
picks manual sync deliberately: state changes in these types are discrete events (a process
starts, a timer pauses, a player gets added to a tracked set), not continuously-varying
values like a transform position, so there's a well-defined moment to trigger a sync rather
than a reason to pay for automatic periodic replication.

## The self-healing resync heartbeat

`Process` layers a second mechanism on top of that: while a process is running, its owner
re-broadcasts its full synced state on a slower, independent cadence (every 5 seconds),
regardless of whether anything actually changed since the last discrete send. The `Process`
source states the reason directly: the discrete, event-driven `[NetworkCallable]`
broadcasts the Core process types use for lifecycle notifications aren't
delivery-confirmed, so a client that missed one has no other way to catch up. The VRChat
networking docs don't promise every network event arrives; the one case they document
explicitly is a client whose own rate limit is exceeded by a sender running a different,
incompatible world version, where [the server "may silently drop events and not deliver
[VRChat describes the two sync
modes](https://creators.vrchat.com/worlds/udon/networking/variables/#2-manual-sync) this way: manual
sync "requires calling `RequestSerialization()` to send data updates" and suits values that don't
change often, while continuous sync "updates automatically" and suits "frequently updated values."
State in the Core types changes in discrete steps, such as a process starting, a timer pausing or a
player joining a tracked set, not continuously like a transform position. Each change has a clear
moment to send it, so there's no reason to pay for automatic periodic replication.

## The resync heartbeat

`Process` adds a second mechanism: while a process runs, its owner sends its full synced state
again every 5 seconds, whether or not anything changed. The lifecycle broadcasts of the Core process
types are discrete `[NetworkCallable]` events with no delivery confirmation, and a client that misses
one has no other way to catch up. The VRChat docs document one case where events are lost: when
players in one instance run different world versions, [the server "may silently drop events and not
deliver
them"](https://creators.vrchat.com/worlds/udon/networking/events/#mismatched-world-versions-in-the-same-instance).
The Core heartbeat isn't scoped only to that one documented case: it treats every discrete
broadcast as unconfirmed and resyncs regardless, which self-heals a missed broadcast within
a few seconds without any acknowledgement or retry logic needed anywhere in the framework.
See [`Process`](../core-concepts/process) for the full mechanics, including how ownership
recovery interacts with this heartbeat when the previous owner leaves mid-run.
The heartbeat doesn't depend on knowing which case happened: a client that missed a broadcast gets
the current synced state within a few seconds, with no acknowledgement or retry logic. See
[`Process`](../core-concepts/process#the-tick-loop) for the loop that sends it.

## Working around what can't be synced directly

The synced tier of `TsvrcMemory` exists specifically because `DataDictionary`, the natural
representation for an arbitrary key-value store, isn't itself a syncable type. [The VRChat
list of types synced variables
support](https://udonsharp.docs.vrchat.com/vrchat-api/#synced-variables) covers primitives,
the small Unity value types (`Vector2`/`3`/`4`, `Quaternion`, `Color`), strings, and `VRCUrl`;
`DataDictionary` isn't among them. Serializing it to a plain string via
[`TsJson`](../utilities/ts-json) before every
synced write, and parsing it back on `OnDeserialization`, is the accommodation: everything
about the manual-sync-plus-`RequestSerialization` pattern above still applies to that string
field itself, exactly as it would to any other synced value. See
[`TsvrcMemory`](../networking-data/tsvrc-memory) for what is and isn't guaranteed about ordering and
delivery for the synced tier specifically; this note doesn't repeat those specifics, only
the shared pattern behind them.

## Ownership follows the same convention throughout

Every one of these types transfers object ownership to the local player before writing a
value it wants that write to propagate (`TsvrcMemory.Set` on a synced key,
`Process.SetProcessOwner`, and so on). [The VRChat ownership
documentation](https://creators.vrchat.com/worlds/udon/networking/ownership/#how-ownership-works)
states plainly that "the owner of a networked object can modify its synchronized Udon
variables," so only the current owner's `RequestSerialization` calls actually produce an
outbound sync for anyone else. This is the same underlying VRChat ownership model in every
case; Core doesn't introduce a second ownership concept of its own, it just applies
the existing VRChat one consistently everywhere state needs to move between clients.
The synced tier of `TsvrcMemory` exists because `DataDictionary`, the natural shape for a key-value
store, isn't a syncable type. [The types synced variables
support](https://udonsharp.docs.vrchat.com/vrchat-api/#synced-variables) are primitives, the small
Unity value types (`Vector2`, `Vector3`, `Vector4`, `Quaternion`, `Color`), strings and `VRCUrl`.
`TsvrcMemory` serializes the dictionary to a string with [`TsJson`](../utilities/ts-json) before
each synced write and parses it back in `OnDeserialization`, and that string field follows the same
manual sync pattern as any other synced value. See [`TsvrcMemory`](../networking-data/tsvrc-memory)
for what the synced tier guarantees about ordering and delivery.

## Ownership

Core uses the [VRChat ownership
model](https://creators.vrchat.com/worlds/udon/networking/ownership/#how-ownership-works) as it is:
only "the owner of a networked object can modify its synchronized Udon variables," so only the
owner sends state to other players. There's no second ownership concept on top of it.

The types differ in how a write from another player reaches the owner:

- `TsvrcMemory` takes ownership for the local player before it writes a synced key.
- `Process` never takes ownership by itself. A lifecycle call from another player is sent to the
owner over the network, and the owner applies it.
- Subclasses that should run on the player who starts them, such as `TsvrcTimer` and
`PlayerTracker`, take ownership with `SetProcessOwner` before starting.
20 changes: 9 additions & 11 deletions docs/core/how-to/showing-player-positions.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,10 @@ How to render a live minimap-style overlay of tracked players, using

## Steps

1. Add a `PlayerPositionOverlay` to your scene and register it as a construct. It's a
[`PlayerTracker`](../players-tracking/player-tracker) underneath, so it needs
`TsConstruct` to run before anything on it works. The renderer doesn't: neither
`PlayerMarkerRenderer` nor `RasterPlayerMarkerRenderer` overrides `TsStart` or touches
`_ts`, so it's safe to just drop one into the scene and assign it, no registration
needed.
1. Add a `PlayerPositionOverlay` to your scene and register it as a construct, so codegen calls
`TsConstruct` on it at startup. The renderer needs no registration: neither
`PlayerMarkerRenderer` nor `RasterPlayerMarkerRenderer` uses `TsStart` or `_ts`, so drop one
into the scene and assign it.
2. Subclass `RasterPlayerMarkerRenderer` to decide what a marker looks like:

```csharp
Expand All @@ -33,7 +31,7 @@ How to render a live minimap-style overlay of tracked players, using
}
```

3. Wire the renderer's output texture to a `RawImage` in your UI, then configure and start
3. Wire the output texture of the renderer to a `RawImage` in your UI, then configure and start
the overlay:

```csharp
Expand All @@ -52,8 +50,8 @@ How to render a live minimap-style overlay of tracked players, using
}
```

4. Add or remove players while it runs with the overlay's own (inherited) tracking methods,
`_overlay.AddTrackedPlayers(playerIds)`/`RemoveTrackedPlayers(playerIds)`. The renderer
4. Add or remove players while it runs with the tracking methods the overlay inherits,
`_overlay.AddTrackedPlayers(playerIds)` and `RemoveTrackedPlayers(playerIds)`. The renderer
picks up the change on its next tick with no extra wiring.

If only a subset of players should ever appear (not everyone in the instance), start the
Expand All @@ -77,8 +75,8 @@ public override void DrawMarker(Color32[] pixelBuffer, int textureWidth, int tex

If you're not drawing to a texture at all (driving 3D world-space marker objects instead,
for example), implement [`PlayerMarkerRenderer`](../ui-components/overlay/player-marker-renderer)
directly rather than its raster subclass. `PlayerPositionOverlay` only ever talks to the
base class's two methods.
directly instead of its raster subclass. `PlayerPositionOverlay` only calls the two methods of the
base class.

## Why this shape

Expand Down
21 changes: 9 additions & 12 deletions docs/core/how-to/tracking-a-group-of-players.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,9 @@ instance", using [`PlayerTracker`](../players-tracking/player-tracker) directly

## When to use which

Reach for `AutoPlayerTracker` first if the roster really is "every player currently in
the instance": it needs no add/remove calls of your own. Use a bare `PlayerTracker`
instead when the roster is a subset you decide, players entering a specific area,
opting into a queue, being invited to a team, since `AutoPlayerTracker` always tracks
everyone and can't be told to track less.
Use `AutoPlayerTracker` when the roster is every player in the instance: it needs no add or remove
calls of your own. Use a plain `PlayerTracker` when the roster is a subset you decide, such as
players in an area, in a queue or on a team, since `AutoPlayerTracker` always tracks everyone.

## Steps

Expand Down Expand Up @@ -58,13 +56,12 @@ everyone and can't be told to track less.
A player who leaves the instance or gets suspended is removed automatically; you never
need to handle that yourself.

3. Read the roster from `LastPlayerIds` inside any `OnTracking*` callback. It's a synced,
owner-managed set: a non-owner's `AddTrackedPlayers`/`RemoveTrackedPlayers` calls
forward to the current owner over the network rather than applying locally.
3. Read the roster from `LastPlayerIds` inside any `OnTracking*` callback. The owner manages the
set: `AddTrackedPlayers` and `RemoveTrackedPlayers` called by another player go to the owner
over the network.

## Why this shape

See the [`PlayerTracker` reference page](../players-tracking/player-tracker) for the full
event table, exactly when a `Notify*` broadcast fires versus when `LastPlayerIds` itself
updates, and the three separate paths that remove a player automatically (leaving,
suspending, or the tracker's own owner abandoning it).
See the [`PlayerTracker` reference page](../players-tracking/player-tracker) for the events, when a
`Notify*` broadcast fires compared to when `LastPlayerIds` updates, and the three cases that remove
a player automatically: leaving, being suspended, or a new owner taking over the tracker.
Loading
Loading