Build a Networked Beer Pong Game in VRChat
Create a reusable multiplayer table game with a throwable pickup, cup triggers, synchronized scores, alternating turns, and a clean round reset. The video demonstrates the original CyanTrigger build; the guide below explains the VRChat networking decisions that keep every player on the same score.
Finish one cup and one locally working ball before adding networking, the full rack, turn rules, or visual polish.
- Build the table, ball, one cup, and a separate scoring trigger.
- Add one authoritative game controller and synchronize durable round state.
- Test scoring, missed shots, resets, ownership changes, and late joins with multiple clients.
The ball can change owners as players pick it up, but the score needs one authority. Send scoring attempts to the game controller owner; only that owner should accept a cup, advance the turn, and serialize the new state.
Video Companion
Creator: Fionna
Video: Beer Pong for CyanTrigger
Scene Layout
Keep detection, presentation, and game state in separate objects. This makes it possible to disable a scored cup without disrupting the table or controller.
BeerPongGame
├── BallSpawn
├── Ball
│ ├── Rigidbody
│ ├── Collider
│ ├── VRC Pickup
│ └── VRC Object Sync
├── TeamA
│ ├── Cup_00
│ │ └── ScoreTrigger
│ └── Cup_01 ...
├── TeamB
│ ├── Cup_00
│ │ └── ScoreTrigger
│ └── Cup_01 ...
├── GameController
├── ScoreDisplay
└── ResetButton
Use a normal collider for the visible cup and a separate trigger collider just inside its opening. Unity calls OnTriggerEnter when the ball collider enters that trigger, provided both objects have colliders, at least one collider is a trigger, and at least one is attached to a physics body. UdonSharp supports Unity's OnTriggerEnter event.
Configure the Ball
A VRC Pickup requires a Rigidbody and a collider. Add VRC Object Sync so VRChat can synchronize the ball's position, rotation, gravity, and kinematic state.
| Component | Purpose | Setup check |
|---|---|---|
| Rigidbody | Gives the ball physics motion | Keep gravity enabled for normal throws |
| Collider | Handles table, cup, and trigger contact | Match the ball closely without making the collider oversized |
| VRC Pickup | Lets players hold and throw the ball | Tune proximity, grip, and throw behavior on each supported input method |
| VRC Object Sync | Synchronizes ball motion | Confirm the current holder owns the ball while manipulating it |
| Ball logic | Marks the beginning and end of a shot | Prevent a single throw from producing more than one accepted score |
Place BallSpawn above a clear part of the table. VRC Object Sync.Respawn() returns the object to its initial position and rotation and clears its velocity, so an owner-controlled respawn is suitable for returning the ball after a completed shot.
Detect a Cup Without Double Scoring
Give every cup a stable team and cup index. The cup trigger reports a possible hit; it does not directly change the scoreboard.
Use this validation order on the game controller owner:
- Confirm the round is active.
- Confirm the reported cup belongs to the team currently being targeted.
- Confirm that cup is still active.
- Confirm this shot has not already been accepted.
- Mark the cup inactive, update the score, lock the current shot, and advance the game state.
- Request serialization, then update cup visuals from the accepted state.
The shot lock matters because physics can generate more than one trigger callback while the ball moves through a cup. An inactive cup check prevents future shots from scoring it again; a per-shot lock prevents the same ball movement from scoring two overlapping triggers.
Before reporting a score, confirm that the entering collider belongs to the game ball. A hand, loose pickup, or unrelated physics object should not be able to remove a cup.
Keep One Authoritative Score
VRChat only permits an object's owner to modify its synchronized variables. Put the durable round state on GameController, choose manual synchronization for deliberate state changes, and call RequestSerialization() after the owner accepts a score or reset.
Ball enters a cup trigger
↓
Cup reports team, cup index, and current shot
↓
Request reaches GameController owner
↓
Owner validates the request
↓
Owner updates cup state, score, turn, and shot state
↓
Owner requests serialization
↓
Every client rebuilds cup visuals and UI from synced state
Network events can carry a scoring request to the controller owner, but the accepted result belongs in synchronized variables. Events are temporary messages; synchronized variables retain the latest values for players who join later.
Define the Round State
Store enough synchronized state to rebuild the table without replaying earlier events.
| State | Ball | Cups and score | Allowed action |
|---|---|---|---|
| Ready | At spawn | Initial rack | Begin the round |
| Team A turn | Available to Team A | Current accepted state | Team A throws |
| Team B turn | Available to Team B | Current accepted state | Team B throws |
| Round complete | Locked or returned | Winning state visible | Start a new round |
Useful durable values include:
- the current phase or turn;
- which cups remain active for each team;
- each team's score;
- a round or shot identifier used to reject stale reports;
- whether the round is complete.
The exact turn rule is a design choice. Decide what counts as a miss, when control passes to the other team, and whether a scored shot grants another throw. Apply that rule only on the controller owner so two clients cannot advance the turn independently.
Finish a Shot and Return the Ball
Scoring is only one way a throw ends. Define a miss volume below and around the table, plus a timeout or rest check for balls that stop somewhere reachable.
For each finished shot:
- Lock further scoring for that shot.
- Apply the score or miss result.
- Advance the turn according to the selected rules.
- Have the ball owner or controller authority respawn the ball.
- Increment the shot identifier and unlock the next throw.
Do not let every client call the reset at once. The authority should perform the state change and clients should display the result they receive.
Apply Cup and Score Visuals
Treat visuals as a view of synchronized state:
- hide, lower, or mark a cup after its active flag changes;
- update both score displays from the synchronized scores;
- show the current team near the pickup area;
- show a clear round-complete state;
- make the reset control available only when its action is valid.
Reapply these visuals when synchronized data is received. A late joiner should see the current rack and turn immediately, without needing the earlier scoring events.
Reset the Rack
The game controller owner should handle the new-round action:
- Reset both teams' active cup state and scores.
- Set the opening turn and active round phase.
- Increment the round or shot identifier.
- Serialize the new state.
- Respawn the ball through its
VRC Object Sync.
If ownership changes because the controller owner leaves, the new owner must continue from the synchronized values already on the controller. Do not initialize the rack unconditionally whenever ownership transfers.
Multiplayer Test Matrix
Test in ClientSim for fast iteration, then use a VRChat instance with at least two clients for the networking pass.
| Test | Expected result |
|---|---|
| Player A throws and scores | Both clients remove the same cup and show the same score |
| Ball crosses the trigger more than once | Only one score is accepted for the shot |
| Ball touches two cup triggers | At most one valid cup is accepted |
| Player B joins mid-round | Current cups, score, turn, and round state appear correctly |
| Controller owner leaves | A new owner continues the existing round |
| Ball leaves the table | The shot ends and the ball returns once |
| New round is requested twice | The rack resets once and clients remain synchronized |
| Desktop, VR, and mobile input | The ball is reachable, grabbable, and throwable on supported platforms |
Help! Every client shows a different score.
Check whether each cup is changing local state directly. Route the scoring attempt to the game controller owner, update synchronized variables there, and request serialization after the accepted change.
Help! One throw scores twice.
Reject a report when the cup is already inactive or the current shot is locked. Keep the lock active until the ball has been returned and the next shot identifier has been assigned.
Help! Unrelated objects can score.
Validate the collider against the known game ball before sending a scoring request. Keep the cup's scoring trigger separate from its solid collision collider so the detection volume is easy to inspect.
Help! The ball jitters or snaps between players.
Inspect ownership while the pickup is held and released. Avoid having the game controller and pickup logic repeatedly take ownership from each other during a throw.
Help! Late joiners see a full rack.
Do not rely on past network events to remove cups. Store the active cup state in synchronized variables and rebuild every cup and score display when that state is deserialized.
Help! The ball never registers inside a cup.
Confirm both objects have colliders, the cup detector has Is Trigger enabled, and the ball has a Rigidbody. Enlarge or reposition the inner trigger so a normal throw crosses it without touching neighboring cups.
Help! The game resets when the owner leaves.
Do not initialize the round during every ownership transfer. The replacement owner should preserve the synchronized cup, score, turn, and round values already received.
Official References
- VRC Pickup
- VRC Object Sync
- VRChat networking
- Network variables
- Object ownership
- Network events
- Late joiners and synchronization
- UdonSharp events
- Unity OnTriggerEnter
Related Guides
- Networked Spin the Bottle
- Collision Audio with CyanTrigger
- Udon Networking Decision Guide
- ClientSim World Testing
- VRChat Prefabs
- World Creation
Topics: VRChat worlds, CyanTrigger, Udon, multiplayer games, pickups, networking