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.

Build order

Finish one cup and one locally working ball before adding networking, the full rack, turn rules, or visual polish.

  1. Build the table, ball, one cup, and a separate scoring trigger.
  2. Add one authoritative game controller and synchronize durable round state.
  3. Test scoring, missed shots, resets, ownership changes, and late joins with multiple clients.
Networking rule

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:

  1. Confirm the round is active.
  2. Confirm the reported cup belongs to the team currently being targeted.
  3. Confirm that cup is still active.
  4. Confirm this shot has not already been accepted.
  5. Mark the cup inactive, update the score, lock the current shot, and advance the game state.
  6. 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.

Identify the ball explicitly

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:

  1. Lock further scoring for that shot.
  2. Apply the score or miss result.
  3. Advance the turn according to the selected rules.
  4. Have the ball owner or controller authority respawn the ball.
  5. 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:

  1. Reset both teams' active cup state and scores.
  2. Set the opening turn and active round phase.
  3. Increment the round or shot identifier.
  4. Serialize the new state.
  5. 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

Related Guides

Topics: VRChat worlds, CyanTrigger, Udon, multiplayer games, pickups, networking