
ShrinkingZone
A closing perimeter that shrinks over time, damaging players caught outside it based on their max health. Includes a looping sound effect while the ring is actively collapsing.ShrinkingZone — ROUNDS Mod
A closing "safe zone" for ROUNDS: a circular perimeter that shrinks over time, damaging any player caught outside it (battle-royale ring style).
Files
ShrinkingZonePlugin.cs— BepInEx plugin entry point, hooks round start/end.ZoneController.cs— spawns the ring visual, shrinks it, and damages players outside it.ShrinkingZoneMod.csproj— build project; references the 4 game DLLs needed to compile.manifest.json— Thunderstore package manifest.
Requirements
- BepInEx (5.4.x) installed for ROUNDS
- UnboundLib — round lifecycle hooks
- (Optional) ModdingUtils — extra player/health helpers if you want cleaner damage calls
- Visual Studio / Rider / VS Code with the .NET SDK
- References to
Assembly-CSharp.dll,UnityEngine.dll,BepInEx.dll, andUnboundLib.dllfrom your ROUNDS install (ROUNDS/BepInEx/plugins/andROUNDS/ROUNDS_Data/Managed/)
Verified against Assembly-CSharp.dll / UnboundLib.dll via dnSpy
Every field/method/attribute name below has been confirmed against the real game and library assemblies — no more guessing left in this mod. Kept here as a reference in case you update BepInEx/UnboundLib/the game later and need to re-check anything that might have changed.
How to enumerate active players✅ Confirmed:PlayerManager.instance.playersis apublic List<Player> players. Code already matches this exactly.How to apply damage, and how to read max health✅ Confirmed:Player.datais aCharacterDatawith plain public floatshealthandmaxHealth.- Damage is dealt through a
DamagableEvent(subclass of abstractDamagable) sitting on the player's GameObject — accessed viaplayer.GetComponent<DamagableEvent>()since no cached field was found. - Real signature:
TakeDamage(Vector2 damage, Vector2 damagePosition, GameObject damagingWeapon = null, Player damagingPlayer = null, bool lethal = true, bool ignoreBlock = false). Notedamageis a Vector2 (magnitude = amount, direction = knockback), not a float —ZoneController.ApplyZoneDamagenow builds this vector pointing back toward the zone center.
Round start/end hook names✅ Confirmed:GameModeHooksis apublic static classholding plainconst stringidentifiers —HookRoundStart = "RoundStart"andHookRoundEnd = "RoundEnd"— exactly matching whatShrinkingZonePlugin.csalready uses.
All three items above are confirmed — see the "Multiplayer / networking" section below for the remaining (now also resolved) networking-specific checks.
Tuning
All the knobs are public static fields at the top of ShrinkingZonePlugin:
StartRadius = 30f;
FinalRadius = 3f;
ShrinkStartDelay = 5f;
ShrinkDuration = 25f;
DamagePercentPerSecond = 0.05f; // 5% of the player's MAX health per second
DamageTickInterval = 0.5f;
Damage scales off each player's max health rather than a flat number, so the zone hurts proportionally regardless of health-boosting cards a player has picked up. At the defaults, a player fully outside the zone loses 5% of their max health every second (i.e. dies in ~20s of standing outside it).
Multiplayer / networking
The zone is now host-authoritative:
- Only the master client (
PhotonNetwork.IsMasterClient) advances the shrink timer and computes the "true" radius. - Every
SyncInterval(default 0.2s) the host broadcasts(center, radius, timer)to all clients viaNetworkingManager.RPC(...), landing on the static[UnboundRPC] RPCA_SyncZoneStatemethod on every machine. - Non-host clients never compute their own radius — they just render whatever the last synced value was. This is what keeps everyone's ring visually identical instead of drifting apart from frame-rate/timing differences.
- Damage is still applied locally per-client, but only for players that
client actually owns (
PhotonView.IsMine), to avoid multiple machines applying the same damage tick to the same player.
Things to verify for the networking layer specifically
✅ Confirmed:NetworkingManager.RPCsignature
Code updated to passpublic static void RPC(Type targetType, string methodName, RaiseEventOptions options, SendOptions sendOptions, params object[] data)RaiseEventOptions.DefaultandSendOptions.SendReliablebefore the trailing float args. Addedusing ExitGames.Client.Photon;andusing Photon.Realtime;for these types (standard PUN2 namespaces) — if the build can't find them, recheck their actual namespace in dnSpy.✅ Confirmed:[UnboundRPC]public class UnboundRPC : Attributein namespaceUnboundLib.Networking— already covered by the existingusing UnboundLib.Networking;at the top ofZoneController.cs. No changes needed.✅ UsingPhotonViewonPlayerplayer.GetComponent<PhotonView>()— no cached field was found, so this resolves it the same way asDamagableEvent.- Does
HookRoundStartfire on all clients or just host? — the one item we haven't explicitly confirmed. If it turns out to be host-only in your UnboundLib version, theSpawnZonecall inShrinkingZonePlugin.OnRoundStartneeds to be wrapped in its own RPC so every client creates a localZoneControllerto receive syncs. Easiest way to check: just build and test in a 2-player session — if the non-host player never sees the ring at all, this is why.
Everything else in this mod has now been verified against the real assemblies via dnSpy — the pattern (host computes truth, broadcasts on an interval, clients render/apply damage locally) is standard for ROUNDS mods, and the exact method/field names are locked in.
Building
-
Open
ShrinkingZoneMod.csprojand edit the<GamePath>property near the top to point at your actual ROUNDS install folder, e.g.:<GamePath>C:\Program Files (x86)\Steam\steamapps\common\ROUNDS</GamePath>Everything else (Managed folder, plugins folder) is derived from this.
-
Double-check the reference filenames actually match what's on disk — open your
ROUNDS_Data\ManagedandBepInEx\pluginsfolders and confirm:Photon3Unity3D.dll,PhotonUnityNetworking.dll,PhotonRealtime.dll, andUnityEngine.CoreModule.dllexist with those exact names (Unity/Photon DLL naming varies a bit between versions — if a name doesn't match, update the<HintPath>in the.csprojaccordingly).- The
UnboundLibsubfolder name underBepInEx\plugins\matches what's in the.csproj(Willis81808-UnboundLibis a guess based on common naming — yours may differ, especially if you installed via a mod manager that names folders differently).
-
Build:
dotnet build -
Copy the resulting
ShrinkingZoneMod.dll(inbin/Debug/netstandard2.1/) intoROUNDS/BepInEx/plugins/. -
Launch the game and check the BepInEx console window (should pop up alongside the game if console logging is enabled in your BepInEx config) — any reference/load errors will show up there immediately.
Packaging for Thunderstore
Zip manifest.json, an icon (icon.png, 256x256), a README.md, and the built
.dll together, then upload via the Thunderstore package manager.