Build a Networked Spin the Bottle Game in VRChat
A multiplayer bottle game needs one shared result. If every player chooses a random angle or simulates the same physics spin independently, the bottle can point somewhere different on each client. This guide uses one owner-authoritative target index, synchronized round state, and a local rotation animation that ends at the same result for everyone.
Synchronize the decision, then animate that decision locally on every client.
- Place numbered target positions evenly around a table.
- Create one owner-controlled game behaviour with synced round and result variables.
- Route each spin request to the owner and reject requests while a round is active.
- Let the owner choose one target index and serialize it.
- Rotate the bottle to that target on every client and verify late-join state.
VRCTween can animate rotation in Udon Graph and UdonSharp, but its tweens are local and do not synchronize automatically. Network the result and round state, not the animation handle.
Video Companion
Fionna's CyanTrigger tutorial demonstrates the original Spin the Bottle interaction. Use it for the visual construction sequence, then apply the owner, state, animation, and late-join workflow in this guide.
Video creator: Fionna
Choose the Spin Model
| Model | Strength | Main risk |
|---|---|---|
| Indexed deterministic spin | Every client receives one result and animates to a known target | Requires a defined list of table positions. |
| Free physics spin | Natural-looking Rigidbody motion | Final direction can be difficult to interpret and reproduce consistently across clients. |
| Hybrid spin | Deterministic result with a physics-like or tweened animation | Requires a clear separation between visual motion and authoritative result. |
The indexed model is the most direct multiplayer implementation. The bottle may rotate several full turns, but its final target comes from synchronized game state.
Build the Table Layout
Create a stable table hierarchy:
SpinTheBottleGame
BottlePivot
BottleVisual
SpinButton
TargetPositions
Target_00
Target_01
Target_02
Target_03
...
StatusDisplay
Use empty target transforms or clearly labelled markers around the table. Keep their order stable because the synchronized result is an array index.
Each target should define:
- the direction the bottle must face
- the label shown to players
- an optional highlight object
- an optional occupancy or eligibility state if the game implements one
Do not reorder the target array in only one platform version of the world. A shared index must describe the same table position in every compatible build.
Create the Round State
Use a small state model:
| State | Meaning | Allowed input |
|---|---|---|
| Idle | The game is ready for a new round | Accept one spin request. |
| Spinning | A result has been chosen and the animation is running | Reject additional requests. |
| Settled | The bottle points at the current result | Allow reset or the next round according to the game rules. |
Store only the values every client needs:
- round phase
- selected target index
- round number or revision
- optional spinner player ID when the design displays it
Manual synchronization is suitable for these infrequent state changes. The owner calls RequestSerialization() after updating the synchronized values.
Use One Authority
Only one client should choose the result.
Two supported request patterns are:
| Pattern | Flow |
|---|---|
| Transfer ownership | The interacting player becomes owner, checks the round, chooses the result, and serializes it. |
| Route to current owner | Any player sends a bounded request to NetworkEventTarget.Owner; the owner validates and starts the round. |
Routing to the current owner avoids moving ownership for every button press. VRChat documents owner-targeted network events as a way to ask the owner to update synchronized state without transferring ownership.
Whichever pattern you use:
- validate the current phase on the owner
- ignore a request if a round is already spinning
- choose the result once
- serialize the result before relying on remote animation
- apply the same state locally on the owner
Choose a Target
For a fixed table, choose an integer index from the available target range.
Unity's integer Random.Range(minInclusive, maxExclusive) excludes the upper bound. For an array of targetCount positions, the candidate range begins at zero and ends before targetCount.
Validate before selecting:
- the target array is not empty
- every indexed target exists
- the eligible target list contains at least one entry when eligibility filtering is used
- the chosen index is valid before reading the target transform
Generate the random result only on the authority. Do not ask every client to run its own random selection.
Convert the Target to a Rotation
The bottle should rotate around a dedicated pivot.
For each selected target:
- determine the horizontal direction from the pivot to the target
- calculate the final yaw that points the bottle's forward axis toward that direction
- add several full turns to the visual animation
- preserve the exact synchronized final target
Keep the bottle mesh's forward direction documented. If the model points along a different local axis, correct the model under BottleVisual rather than hiding an unexplained offset throughout the game logic.
Animate with VRCTween
VRCTween supports TweenRotation and TweenLocalRotation for GameObjects and Transforms.
Use the animation as presentation:
received round result
→ calculate final target rotation
→ begin local rotation tween
→ update status to Spinning
→ finish at the exact target
→ show result and enable the next allowed action
Choose one easing curve that slows clearly near the result. Store the tween handle when the implementation needs to cancel or replace an interrupted animation.
VRCTween handles are local to each scene instance. Do not attempt to synchronize the handle itself.
Synchronize Result, Not Frames
Avoid synchronizing the bottle's rotation every frame for the deterministic model.
Synchronize:
- selected target index
- phase
- round revision
Derive locally:
- tween duration
- full visual rotations
- sound and particle timing
- target highlight
- displayed target label
Every client can produce a smooth local animation from the same compact result. At completion, snap or set the bottle to the exact final target rotation so accumulated visual differences do not remain.
Apply Network Updates
Use one method to apply the synchronized state:
ApplyRoundState
→ validate phase and target index
→ update button availability
→ animate or set bottle rotation
→ update target highlight
→ update status display
Call it:
- after the owner changes state
- after deserialization on remote clients
- when restoring the current result for a late joiner
Do not make the network event the only way clients learn the result. VRChat does not replay past events for late joiners, while synchronized variables are delivered with their latest values.
Handle Late Joiners
A player who joins during or after a round needs a coherent table:
| Received phase | Late-join behaviour |
|---|---|
| Idle | Show the ready state. |
| Spinning | Either animate toward the selected result or place the bottle at the result and show that the round is active. |
| Settled | Place the bottle directly at the selected target and show the current result. |
Do not invent a new result for the late joiner. Apply the synchronized target index and phase.
If exact mid-spin timing matters, synchronize an authoritative start time and derive remaining animation time. If it does not, favour a simpler catch-up rule that always ends at the correct result.
Prevent Double Spins
Use phase validation on both the requesting client and the owner:
- disable or visually lock the spin control after a request
- validate again on the owner
- accept only the first valid request
- reject all others until the round permits another spin
The owner check is authoritative. Local button disabling improves feedback but cannot resolve simultaneous requests by itself.
Add Result Feedback
After the bottle settles:
- highlight the selected target
- show its label on a status display
- play a short spatialized sound
- clear the previous target highlight
- enable reset or next-round input
Keep feedback derived from the selected index. A network event may be used for a temporary flourish, but the visible current result should be reconstructable from synchronized state.
Reset the Round
Define what reset means:
| Reset mode | Result |
|---|---|
| Ready reset | Clear highlight, return the bottle to its starting rotation, and set phase to Idle. |
| Keep-result next round | Leave the bottle at the last target and allow the next spin from that angle. |
| Timed reset | The owner returns the game to Idle after the configured round interval. |
| Manual moderator reset | A clearly labelled control restores the authoritative state. |
The owner updates and serializes the reset phase. Every client applies the same result through the shared state method.
Optional Physics Variant
For a grabbable or physically spun bottle:
- add a Rigidbody and Collider
- add VRC Pickup if players hold it
- add VRC Object Sync when the transform must be synchronized
- establish which owner controls its physics
- detect when the bottle has settled
- convert the final direction to the nearest indexed target
- synchronize that target as the official result
Use the physical transform as input to the final decision, not as the only record of the round. A synced target index still gives late joiners and UI systems a stable answer.
Test ownership changes carefully. VRC Object Sync synchronizes transform and Rigidbody-related state, but your round phase and selected target remain custom game variables.
Participation and Empty Positions
Choose one documented rule:
- all table positions are always eligible
- only marked occupied positions are eligible
- players opt into a position before the round
- the bottle may point at an empty position and the group decides what happens
If eligibility changes during a round, the owner should use a stable snapshot for that selection. Do not silently remap an already synchronized target index because a player moves afterward.
Cross-Platform Interaction
- Make the spin control usable with desktop mouse interaction.
- Keep the button reachable with VR controller pointing.
- Provide a touch-friendly world UI control when the Android experience needs it.
- Do not require a player to physically flick the bottle unless every supported input mode has an alternative.
- Keep status labels readable from the table positions.
- Avoid rapid flashing feedback.
- Use spatial audio with a Far distance appropriate to the table.
Test the Game
| Test | Expected result |
|---|---|
| First spin | One target is selected and every client reaches the same result. |
| Simultaneous requests | The owner accepts only one round. |
| Repeated input | Spin requests are rejected while phase is Spinning. |
| Every target | The bottle can point correctly at every indexed position. |
| Late join during spin | The new player catches up to the synchronized round. |
| Late join after spin | The new player sees the settled result. |
| Owner leaves | The new owner can reset and begin another round. |
| Reset | Phase, bottle rotation, highlight, and status agree on every client. |
| Desktop, VR, and mobile | Every supported input mode has a usable spin control. |
| PC and Android builds | Target ordering and result meaning are identical. |
Use ClientSim for local event, tween, and state-machine checks. Use multiple VRChat clients or a private instance for ownership, request races, deserialization, and late-join behaviour.
Release Checklist
- [ ] Target transforms are ordered and labelled.
- [ ] Target ordering matches across platform builds.
- [ ] Only the authority chooses the random result.
- [ ] Integer random selection uses the correct exclusive upper bound.
- [ ] Round phase and selected index are synchronized.
- [ ] Manual changes call
RequestSerialization(). - [ ] Spin requests are validated on the owner.
- [ ] Repeated requests are locked during the active round.
- [ ] VRCTween is treated as local presentation.
- [ ] Every client finishes at the exact selected target.
- [ ] Late joiners reconstruct the current result from synced state.
- [ ] Reset, owner-leave, desktop, VR, mobile, PC, and Android tests pass.
Troubleshooting
Different players see different results.
Choose the random index only on the owner, store it in a synced variable, call RequestSerialization for manual sync, and animate from that received index on every client.
The bottle points between two positions.
Check the BottleVisual forward axis, pivot position, and target transforms. Finish the tween by applying the exact rotation calculated from the selected target.
Two spins begin at the same time.
Validate the phase on the owner before selecting a result. Local button locking alone cannot resolve two requests that arrive close together.
The tween works locally but remote clients do not move.
VRCTween is local and does not synchronize automatically. Send the selected index through synchronized state and start the local tween after each client receives it.
Late joiners see the bottle at its default rotation.
Apply the latest synchronized phase and target index after deserialization. Past network events are not replayed for late joiners.
The new owner cannot start a round after the previous owner leaves.
Handle ownership transfer without tying control to a saved player reference. Re-evaluate the synchronized phase and expose reset when the inherited state is incomplete.
PC and Android players point at different labelled positions.
Ensure both platform builds use the same blueprint and identical target-array ordering. The same synchronized index must identify the same table position in each build.
Official References
- VRChat Udon Networking
- Network Variables
- Network Events
- Object Ownership
- Late Joiners and Sync Issues
- VRCTween
- VRCTween Rotation Types
- VRC Object Sync
- Unity Random.Range