Переглянути джерело

Fix sync modes, null handling and per-script bugs; compile check (1.0.2)

- Compile the package's U# programs when they arrive uncompiled (on load and on reimport)
- NoVariableSync on every script without synced variables
- JumpPad: keep the inspector's Animator; don't keep the limited speeds after a mid-air relaunch; world gravity
- PickupMovementFixer: leave pickups to their own physics while the target is still; skip held pickups
- LaserBeam: draw in world space; LaserReceiver: exact rotated boxes, no per-frame allocation
- Owner-only physics for synced pickups; sharedMaterials instead of material copies
- Checkpoint progress is per player; docs for six more scripts; README components table

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ArtyomV2X 1 місяць тому
батько
коміт
222123d92e

+ 12 - 11
Documentation~/BoneLookAtPLayer.md → Documentation~/BoneLookAtPlayer.md

@@ -1,12 +1,13 @@
-# Bone Look At Player
-Intended as a rotation for objects like cameras, NPCs, or other items that should face the nearest player.
-
-## Requires:
-- Nothing
-
-## Object To Rotate - Type: GameObject
-- Takes a bone GameObject (or any other GameObject for all I care) and sets it as the target to rotate.
-## Rotation Speed - Type: Float
-- Limits the rotation speed to this value in degrees per second.
-## Rotation Offset - Type: Vector3
+# Bone Look At Player
+Intended as a rotation for objects like cameras, NPCs, or other items that should face the nearest player.
+
+## Requires:
+- Nothing
+
+## Object To Rotate - Type: GameObject
+- Takes a bone GameObject (or any other GameObject for all I care) and sets it as the target to rotate.
+- If it's left empty, the script logs an error and turns itself off.
+## Rotation Speed - Type: Float
+- Limits the rotation speed to this value in degrees per second.
+## Rotation Offset - Type: Vector3
 - In the case that the bone is offset from the root object and needs an offset, the target point will be offset by this amount in degrees per axis of transform.

+ 11 - 9
Documentation~/Checkpoint.md

@@ -1,9 +1,11 @@
-# Checkpoint
-
-Intended as a save/load system, though ended up just moving the respawn location to prevent stinky cheaters or accidental softlocks.
-
-## Requires:
-- Collider set to Is Trigger
-
-## Target Object - Type: GameObject
-- Intended to be used for the VRCWorld component to track progress and move the respawn location for the local player.
+# Checkpoint
+
+Intended as a save/load system, though ended up just moving the respawn location to prevent stinky cheaters or accidental softlocks.
+
+## Requires:
+- Collider set to Is Trigger
+
+## Target Object - Type: GameObject
+- Intended to be used for the VRCWorld component to track progress and move the respawn location for the local player.
+
+Each player's progress is their own: a checkpoint moves the target only for the player who walks into it, and then hides itself for them.

+ 53 - 42
Documentation~/ImpactSoundPlayer.md

@@ -1,42 +1,53 @@
-## Impact Sound Player
-
-Diegetic sounds can be tough and scaling only volume with impact force doesn't sound right. May as well have three distinct sounds, right?
-
-## Requires:
-- **VRC Pickup**
-- **VRC Object Sync**
-- **Rigidbody**
-- **Audio Source attached to GameObject (3)**
-
-## Small Impact Sound Object - Type: GameObject
-- Enables the GameObject under the condition that the velocity on impact with another collider is less than the **Medium Impact Threshold**.
-
-## Medium Impact Sound Object - Type: GameObject
-- Enables the GameObject under the condition that the velocity on impact with another collider is more than the **Medium Impact Threshold**, but less than the **Large Impact Threshold**.
-
-## Large Impact Sound Object - Type: GameObject
-- Enables the GameObject under the condition that the velocity on impact with another collider is more than the **Large Impact Threshold**.
-
-## Medium Impact Threshold - Type: Float
-- The velocity requirement for a **Medium Impact Sound Object** to trigger instead of a **Small Impact Sound Object**.
-  
-- **Default Value:** `5.0`
-
-## Large Impact Threshold - Type: Float
-- The velocity requirement for a **Large Impact Sound Object** to trigger instead of a **Medium Impact Sound Object**.
-  
-- **Default Value:** `10.0`
-
----
-
-### Usage Instructions:
-1. Attach this script to a GameObject in Unity.
-2. Assign the respective GameObjects for the **Small**, **Medium**, and **Large Impact Sound Objects**.
-3. Set the desired thresholds for **Medium Impact Threshold** and **Large Impact Threshold**.
-4. Ensure the GameObject has the required components: **Rigidbody**, **VRC Pickup**, and **VRC Object Sync**.
-
-### Script Behavior:
-- When a collision occurs, the script evaluates the impact velocity:
-  - If the velocity is **less than the Medium Impact Threshold**, the **Small Impact Sound Object** is activated.
-  - If the velocity is **between the Medium and Large Impact Thresholds**, the **Medium Impact Sound Object** is activated.
-  - If the velocity is **greater than the Large Impact Threshold**, the **Large Impact Sound Object** is activated.
+## Impact Sound Player
+
+Diegetic sounds can be tough and scaling only volume with impact force doesn't sound right. May as well have three distinct sounds, right?
+
+## Requires:
+- **VRC Pickup**
+- **VRC Object Sync**
+- **Rigidbody**
+- **Audio Source attached to GameObject (3)**
+
+## Small Impact Sound Object - Type: GameObject
+- Enables the GameObject under the condition that the velocity on impact with another collider is less than the **Medium Impact Threshold**.
+
+## Medium Impact Sound Object - Type: GameObject
+- Enables the GameObject under the condition that the velocity on impact with another collider is more than the **Medium Impact Threshold**, but less than the **Large Impact Threshold**.
+
+## Large Impact Sound Object - Type: GameObject
+- Enables the GameObject under the condition that the velocity on impact with another collider is more than the **Large Impact Threshold**.
+
+## Medium Impact Threshold - Type: Float
+- The velocity requirement for a **Medium Impact Sound Object** to trigger instead of a **Small Impact Sound Object**.
+  
+- **Default Value:** `5.0`
+
+## Large Impact Threshold - Type: Float
+- The velocity requirement for a **Large Impact Sound Object** to trigger instead of a **Medium Impact Sound Object**.
+  
+- **Default Value:** `10.0`
+
+## Minimum Impact - Type: Float
+- Impacts slower than this play no sound at all. Raise it a little (around `0.5`) to stop resting objects clicking.
+
+- **Default Value:** `0` (every contact plays a sound)
+
+## Cooldown - Type: Float
+- Seconds after a sound before the next one can play.
+
+- **Default Value:** `0` (no limit)
+
+---
+
+### Usage Instructions:
+1. Attach this script to a GameObject in Unity.
+2. Assign the respective GameObjects for the **Small**, **Medium**, and **Large Impact Sound Objects**.
+3. Set the desired thresholds for **Medium Impact Threshold** and **Large Impact Threshold**.
+4. Ensure the GameObject has the required components: **Rigidbody**, **VRC Pickup**, and **VRC Object Sync**.
+
+### Script Behavior:
+- When a collision occurs, the script evaluates the impact velocity:
+  - If the velocity is **less than the Minimum Impact**, or the **Cooldown** hasn't passed, nothing plays.
+  - If the velocity is **less than the Medium Impact Threshold**, the **Small Impact Sound Object** is activated.
+  - If the velocity is **between the Medium and Large Impact Thresholds**, the **Medium Impact Sound Object** is activated.
+  - If the velocity is **greater than the Large Impact Threshold**, the **Large Impact Sound Object** is activated.

+ 58 - 57
Documentation~/JumpPad.md

@@ -1,57 +1,58 @@
-## Jump Pad Controller
-
-This script provides a fully functional Jump Pad system for use in VRChat worlds. The Jump Pad propels players or objects towards a target with a smooth arc, complete with optional animation and debug logging.
-
-## Requires:
-- **Animator** component (optional but recommended for visual feedback)
-- **Colliders** for trigger detection
-
-## Debug - Type: Boolean
-- Enables or disables debug messages for troubleshooting.
-
-## Launch Animation - Type: Animator
-- The Animator used to play a launch animation.
-- **Default Parameter Name:** `isLaunch`
-
-## Jump Target - Type: Transform
-- Specifies the target point where the Jump Pad propels the player or object.
-
-## Self - Type: Transform
-- The Transform of the Jump Pad itself, used for calculating the jump arc.
-
-## Arc Time - Type: Float
-- The duration (in seconds) for the arc trajectory.
-  - **Default Value:** `1.2`
-
-## Walk Limit - Type: Float
-- The restricted walk speed for players during the jump.
-  - **Default Value:** `0.2`
-
-## Run Limit - Type: Float
-- The restricted run speed for players during the jump.
-  - **Default Value:** `0.4`
-
-## Strafe Limit - Type: Float
-- The restricted strafe speed for players during the jump.
-  - **Default Value:** `0.2`
-
----
-
-### Usage Instructions:
-1. Attach this script to the Jump Pad GameObject in Unity.
-2. Assign the **Jump Target** Transform to define the destination of the jump.
-3. (Optional) Assign an **Animator** component and set the animation parameter name if different from the default.
-4. Configure the **Arc Time**, **Walk Limit**, **Run Limit**, and **Strafe Limit** as needed.
-
-### Script Behavior:
-- When a player or object enters the Jump Pad:
-  - Calculates and applies the velocity required to reach the **Jump Target**.
-  - Limits the player’s movement for the duration of the jump.
-  - Activates the launch animation (if an Animator is assigned).
-  - Synchronizes the event using **VRC Udon Networking**.
-- Once the jump completes:
-  - Restores the player’s movement to default speeds.
-  - Deactivates the launch animation.
-
-### Debug Mode:
-- Outputs detailed logs to Unity’s console, including calculated velocities and state changes, to assist with testing and fine-tuning.
+## Jump Pad Controller
+
+This script provides a fully functional Jump Pad system for use in VRChat worlds. The Jump Pad propels players or objects towards a target with a smooth arc, complete with optional animation and debug logging.
+
+## Requires:
+- **Animator** component (optional but recommended for visual feedback)
+- **Colliders** for trigger detection
+
+## Debug - Type: Boolean
+- Enables or disables debug messages for troubleshooting.
+
+## Launch Animation - Type: Animator
+- The Animator used to play a launch animation. If it's left empty, the Animator on the Jump Pad's own GameObject is used, if there is one.
+- **Default Parameter Name:** `isLaunch`
+
+## Jump Target - Type: Transform
+- Specifies the target point where the Jump Pad propels the player or object.
+
+## Self - Type: Transform
+- Where the jump arc starts. If it's left empty, the Jump Pad's own Transform is used.
+
+## Arc Time - Type: Float
+- The duration (in seconds) for the arc trajectory.
+  - **Default Value:** `1.2`
+
+## Walk Limit - Type: Float
+- The restricted walk speed for players during the jump.
+  - **Default Value:** `0.2`
+
+## Run Limit - Type: Float
+- The restricted run speed for players during the jump.
+  - **Default Value:** `0.4`
+
+## Strafe Limit - Type: Float
+- The restricted strafe speed for players during the jump.
+  - **Default Value:** `0.2`
+
+---
+
+### Usage Instructions:
+1. Attach this script to the Jump Pad GameObject in Unity.
+2. Assign the **Jump Target** Transform to define the destination of the jump.
+3. (Optional) Assign an **Animator** component and set the animation parameter name if different from the default.
+4. Configure the **Arc Time**, **Walk Limit**, **Run Limit**, and **Strafe Limit** as needed.
+
+### Script Behavior:
+- When a player or object enters the Jump Pad:
+  - Calculates and applies the velocity required to reach the **Jump Target**, using the world's gravity (and, for players, their gravity strength).
+  - Limits the player’s movement for the duration of the jump. Landing back on the pad mid-jump launches again and keeps the limit until the last jump ends.
+  - Activates the launch animation (if an Animator is assigned).
+- Every player launches themselves, and every client plays the animation when anyone uses the pad, so nothing needs to be synced.
+- A pickup is launched by its owner (the player holding it, or the last one who did), and is dropped first.
+- Once the jump completes:
+  - Restores the player’s movement to default speeds.
+  - Deactivates the launch animation.
+
+### Debug Mode:
+- Outputs detailed logs to Unity’s console, including calculated velocities and state changes, to assist with testing and fine-tuning.

+ 33 - 0
Documentation~/LaserBeam.md

@@ -0,0 +1,33 @@
+## Laser Beam Controller
+
+Draws a laser from an origin point to whatever it hits, and can carry an object along to the end of the beam. Pair it with a **Laser Receiver Controller** for laser puzzles.
+
+## Requires:
+- **Line Renderer** (on any GameObject)
+
+## Line Renderer - Type: LineRenderer
+- The Line Renderer that draws the beam. The script switches it to world space, so it can be on any GameObject.
+
+## Laser Origin - Type: Transform
+- Where the beam starts. It fires along this Transform's forward (blue) axis.
+
+## Max Distance - Type: Float
+- How far the beam reaches when it hits nothing.
+- **Default Value:** `100.0`
+
+## Layer Mask - Type: LayerMask
+- The layers the beam stops at. If it's left at **Nothing**, the beam goes through everything (a warning is logged).
+
+## End Point Object - Type: GameObject
+- Moved to the end of the beam every frame. Give it a collider on the **Laser Receiver Controller**'s laser layer so receivers can detect the beam.
+
+## Hit Triggers - Type: Boolean
+- Whether the beam stops at trigger colliders, such as a Laser Receiver's trigger. Leave it on when the End Point Object should land on a receiver.
+- **Default Value:** `true`
+
+---
+
+### Usage Instructions:
+1. Add a Line Renderer with two positions, and this script.
+2. Assign the **Line Renderer** and **Laser Origin**, and set the **Layer Mask**.
+3. (Optional) Assign an **End Point Object** with a small collider on the laser layer (22 by default).

+ 31 - 0
Documentation~/LaserReceiver.md

@@ -0,0 +1,31 @@
+## Laser Receiver Controller
+
+Turns things on while a laser end point is inside its trigger, and off again when it leaves.
+
+## Requires:
+- **Collider** set to Is Trigger (a Box Collider is checked exactly, rotation included; other shapes use the box around them)
+
+## Target Object - Type: GameObject
+- Active while a laser is inside, inactive otherwise. Turned off when the world loads.
+
+## Laser Layer - Type: Int
+- The layer of the laser end point's collider. Colliders on this layer count as a laser, unless they belong to a pickup.
+- **Default Value:** `22`
+
+## Animators / Parameter Names - Type: Arrays
+- Each Animator gets its matching bool parameter set to whether a laser is inside. Empty entries are skipped.
+
+## Audio Object True / Audio Object False - Type: GameObject
+- Enabled when a laser arrives (True) or leaves (False); the other one is disabled.
+
+## Audio Object Loop - Type: GameObject
+- Active while a laser is inside.
+
+## Trigger Collider - Type: Collider
+- The trigger to check. If it's left empty, this GameObject's collider is used.
+
+## Target Renderer / Material True / Material False / Material Index
+- Swaps the Target Renderer's material at **Material Index** to Material True while a laser is inside, and to Material False when it leaves.
+
+## Debug Mode - Type: Boolean
+- Logs every change of the number of lasers inside.

+ 32 - 0
Documentation~/PickupFizzler.md

@@ -0,0 +1,32 @@
+## Pickup Fizzler
+
+A Portal-style fizzler: a pickup that enters it is dropped, glows with the fizzle material, drifts on for a moment, then respawns.
+
+**Note:** This script is still under review, so it's marked obsolete for now. It works, but expect changes.
+
+## Requires:
+- **Collider** set to Is Trigger
+- Pickups with **VRC Object Sync** (it's what respawns them)
+
+## Fizzler Sound - Type: AudioSource
+- Follows the pickup while it fizzles, then returns to where it was.
+
+## Player Enter Sound - Type: AudioSource
+- Played when the local player walks through.
+
+## Pickup Layer - Type: Int
+- Only pickups on this layer fizzle.
+- **Default Value:** `13` (VRChat's Pickup layer)
+
+## Fizzle Duration - Type: Float
+- Seconds between entering the fizzler and respawning.
+- **Default Value:** `2.0`
+
+## Fizzle Material - Type: Material
+- Replaces every material on the pickup while it fizzles.
+
+---
+
+### Script Behavior:
+- One pickup fizzles at a time; others pass through while it does.
+- Every player sees the effect. Only the pickup's owner moves and respawns it, so the sync isn't fought over.

+ 17 - 0
Documentation~/PickupMovementFixer.md

@@ -0,0 +1,17 @@
+## Pickup Following Mover Fix
+
+Keeps pickups riding on a moving object (a lift, a platform, a conveyor) instead of being left behind when it's moved by an Animator or a script.
+
+## Requires:
+- **Collider** set to Is Trigger, covering the space above the moving object where pickups rest
+- The trigger moves with the target (make it a child of it)
+
+## Target Object - Type: GameObject
+- The moving object to follow.
+
+---
+
+### Script Behavior:
+- While the target moves, unheld pickups (on the Pickup layer, 13) inside the trigger move with it.
+- When it stops, they stop with it, then fall and slide on their own as usual.
+- A synced pickup is moved only by its owner.

+ 18 - 0
Documentation~/PickupSpeedLimiter.md

@@ -0,0 +1,18 @@
+## Pickup Speed Limiter
+
+Caps how fast a pickup can fly once it's let go, so throws can't launch it out of the world.
+
+## Requires:
+- **VRC Pickup** and **Rigidbody** on the same GameObject
+
+## Max Speed - Type: Float
+- The fastest the pickup may move, in metres per second, while nobody holds it.
+- **Default Value:** `5.0`
+
+## Debug Speed - Type: Boolean
+- Logs a warning each time the speed is limited.
+
+---
+
+### Script Behavior:
+- Checked every physics step. With **VRC Object Sync**, only the pickup's owner limits it.

+ 22 - 0
Documentation~/TimedTeleport.md

@@ -0,0 +1,22 @@
+## Timed Teleport
+
+Teleports a player a moment after they walk into a trigger.
+
+## Requires:
+- **Collider** set to Is Trigger
+
+## Teleport Target - Type: Transform
+- Where the player goes, facing the way it faces.
+
+## Delay - Type: Float
+- Seconds between entering and teleporting.
+- **Default Value:** `1.0`
+
+## Cancel On Exit - Type: Boolean
+- Leaving the trigger before the delay is up cancels the teleport.
+- **Default Value:** `false`
+
+---
+
+### Script Behavior:
+- Only the player who walks in is teleported. Walking in again while a teleport is waiting doesn't queue another.

+ 8 - 0
Editor.meta

@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 4da563f59d894b77a861dd51a1fffaaf
+folderAsset: yes
+DefaultImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 21 - 0
Editor/AUMSS.Editor.asmdef

@@ -0,0 +1,21 @@
+{
+    "name": "AUMSS.Editor",
+    "rootNamespace": "AUMSS.EditorTools",
+    "references": [
+        "UdonSharp.Runtime",
+        "UdonSharp.Editor",
+        "VRC.Udon",
+        "VRC.Udon.Editor"
+    ],
+    "includePlatforms": [
+        "Editor"
+    ],
+    "excludePlatforms": [],
+    "allowUnsafeCode": false,
+    "overrideReferences": false,
+    "precompiledReferences": [],
+    "autoReferenced": true,
+    "defineConstraints": [],
+    "versionDefines": [],
+    "noEngineReferences": false
+}

+ 7 - 0
Editor/AUMSS.Editor.asmdef.meta

@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: 4e67b7faa9f5476a9ea120313e763814
+AssemblyDefinitionImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 65 - 0
Editor/AUMSSProgramCheck.cs

@@ -0,0 +1,65 @@
+using UdonSharp;
+using UnityEditor;
+using UnityEngine;
+
+namespace AUMSS.EditorTools
+{
+    /// <summary>
+    /// Makes sure AUMSS's U# programs are compiled. The package ships them uncompiled, and U# only compiles when a
+    /// script's source differs from what it last compiled: after an update that changes no script, the programs
+    /// would stay uncompiled, and editor code (U#'s own, or a world's tools) couldn't write to AUMSS components.
+    /// So they're checked whenever the editor loads, and whenever the package's program assets are reimported (an
+    /// update while the editor is open, which reloads nothing if no script changed).
+    /// </summary>
+    [InitializeOnLoad]
+    public static class AUMSSProgramCheck
+    {
+        const string PackagePath = "Packages/dev.av2x.aumss/";
+
+        static AUMSSProgramCheck()
+        {
+            EditorApplication.delayCall += CheckOnLoad;
+        }
+
+        class ImportCheck : AssetPostprocessor
+        {
+            static void OnPostprocessAllAssets(string[] imported, string[] deleted, string[] moved, string[] movedFrom)
+            {
+                foreach (string path in imported)
+                {
+                    if (!path.StartsWith(PackagePath) || !path.EndsWith(".asset")) continue;
+                    // U#'s own compile saves these too; by then they're compiled, so the check does nothing
+                    EditorApplication.delayCall -= CheckOnLoad;
+                    EditorApplication.delayCall += CheckOnLoad;
+                    return;
+                }
+            }
+        }
+
+        static void CheckOnLoad()
+        {
+            if (EditorApplication.isPlayingOrWillChangePlaymode) return;
+            if (EditorApplication.isCompiling || EditorApplication.isUpdating)
+            {
+                EditorApplication.delayCall += CheckOnLoad;
+                return;
+            }
+            // U# can't compile while C# has errors; it gets compiled once they're fixed (a script change, so U# does)
+            if (EditorUtility.scriptCompilationFailed || !NeedsCompile()) return;
+            Debug.Log("[AUMSS] Compiling UdonSharp programs: the package's arrived uncompiled.");
+            UdonSharpProgramAsset.CompileAllCsPrograms(forceCompile: true);
+        }
+
+        /// <summary>True if any of the package's U# programs hasn't been compiled yet.</summary>
+        public static bool NeedsCompile()
+        {
+            foreach (UdonSharpProgramAsset program in UdonSharpProgramAsset.GetAllUdonSharpPrograms())
+            {
+                if (program == null || program.sourceCsScript == null) continue;
+                if (!AssetDatabase.GetAssetPath(program).StartsWith(PackagePath)) continue;
+                if (program.fieldDefinitions == null) return true;
+            }
+            return false;
+        }
+    }
+}

+ 11 - 0
Editor/AUMSSProgramCheck.cs.meta

@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 4faa11e95a40448682e0318c2adfce58
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 21 - 5
README.md

@@ -1,12 +1,28 @@
-# Usage of the scripting system
+# AUMSS - Artyom's Udon Modular Scripting System
 
-## Any scripts not listed here are deprecated and should not be used unless you have some clue what they do or how to fix em.
+UdonSharp utility scripts for VRChat worlds. Every component is under **Add Component → Artyom Scripting System**.
 
-Leave a fix/issue if you can figure out anything cause I sure don't care to change these junky files /shrug
+## Components
+
+| Component | What it does | Docs |
+| --- | --- | --- |
+| Bone Look At Player | Turns an object (a bone, a camera) to face the nearest player | [BoneLookAtPlayer](Documentation~/BoneLookAtPlayer.md) |
+| Checkpoint | Moves the local player's respawn point when they reach it | [Checkpoint](Documentation~/Checkpoint.md) |
+| Impact Sound Player | Plays a small, medium or large sound depending on how hard something hits | [ImpactSoundPlayer](Documentation~/ImpactSoundPlayer.md) |
+| Jump Pad Controller | Launches players and pickups in an arc onto a target | [JumpPad](Documentation~/JumpPad.md) |
+| Laser Beam Controller | Draws a laser to whatever it hits | [LaserBeam](Documentation~/LaserBeam.md) |
+| Laser Receiver Controller | Turns things on while a laser reaches it | [LaserReceiver](Documentation~/LaserReceiver.md) |
+| Linked Object System | Drives Animator bools from interactions and triggers | |
+| Pickup Fizzler | Portal-style fizzler that respawns pickups (under review) | [PickupFizzler](Documentation~/PickupFizzler.md) |
+| Pickup Following Mover Fix | Keeps pickups riding on moving platforms | [PickupMovementFixer](Documentation~/PickupMovementFixer.md) |
+| Pickup Speed Limiter | Caps how fast a thrown pickup can fly | [PickupSpeedLimiter](Documentation~/PickupSpeedLimiter.md) |
+| Timed Teleport | Teleports a player a moment after they walk in | [TimedTeleport](Documentation~/TimedTeleport.md) |
 
-Usage docs for each script are in [Documentation~](Documentation~).
+### Deprecated
 
-### More guides to come eventually.
+**Interact Script**, **Logic Script**, **Pickup Detector** and **Player Trigger** are replaced by the Linked Object System. They're under **Artyom Scripting System → _DEPRECATED_**, and keep working in existing scenes, but shouldn't be used in new ones. ([InteractScript](Documentation~/InteractScript.md) still has its docs.)
+
+Leave a fix/issue if you can figure out anything cause I sure don't care to change these junky files /shrug
 
 ## Installation
 

+ 33 - 34
Runtime/BoneLookAtPlayer.cs

@@ -1,9 +1,8 @@
-using UdonSharp;
+using UdonSharp;
 using UnityEngine;
 using VRC.SDKBase;
-using VRC.Udon;
-using System;
 
+[UdonBehaviourSyncMode(BehaviourSyncMode.NoVariableSync)]
 [DisallowMultipleComponent]
 [AddComponentMenu("Artyom Scripting System/Bone Look At Player")]
 public class BoneLookAtPlayer : UdonSharpBehaviour
@@ -13,60 +12,60 @@ public class BoneLookAtPlayer : UdonSharpBehaviour
     public Vector3 rotationOffset; // Rotation offset for each axis
 
     private Transform objectTransform;
+    private VRCPlayerApi[] players = new VRCPlayerApi[0]; // Reused every frame, grown when more players join
 
     void Start()
     {
-        if (objectToRotate != null)
+        if (objectToRotate == null)
         {
-            objectTransform = objectToRotate.transform;
-        }
-        else
-        {
-            Debug.LogError("Object to rotate not assigned.");
+            Debug.LogError("[AUMSS] Bone Look At Player: Object To Rotate isn't assigned, so nothing will turn.");
+            enabled = false; // Stops Update, which would otherwise throw and halt the script
+            return;
         }
+        objectTransform = objectToRotate.transform;
     }
 
     void Update()
     {
-        VRCPlayerApi nearestPlayer = GetNearestPlayer(); // Method to get the nearest player
-        if (nearestPlayer != null)
+        if (objectTransform == null) return; // Not set up (enabled = false only takes effect next frame)
+        VRCPlayerApi nearestPlayer = GetNearestPlayer();
+        if (nearestPlayer == null) return;
+
+        Vector3 playerHeadPosition = nearestPlayer.GetTrackingData(VRCPlayerApi.TrackingDataType.Head).position;
+
+        // Calculate the direction to look
+        Vector3 directionToLook = playerHeadPosition - objectTransform.position;
+        if (directionToLook != Vector3.zero)
         {
-            Vector3 playerHeadPosition = nearestPlayer.GetTrackingData(VRCPlayerApi.TrackingDataType.Head).position;
+            Quaternion targetRotation = Quaternion.LookRotation(directionToLook);
 
-            // Calculate the direction to look
-            Vector3 directionToLook = playerHeadPosition - objectTransform.position;
-            if (directionToLook != Vector3.zero)
-            {
-                Quaternion targetRotation = Quaternion.LookRotation(directionToLook);
-                
-                // Apply the rotation offset
-                targetRotation *= Quaternion.Euler(rotationOffset);
+            // Apply the rotation offset
+            targetRotation *= Quaternion.Euler(rotationOffset);
 
-                objectTransform.rotation = Quaternion.RotateTowards(objectTransform.rotation, targetRotation, rotationSpeed * Time.deltaTime);
-            }
+            objectTransform.rotation = Quaternion.RotateTowards(objectTransform.rotation, targetRotation, rotationSpeed * Time.deltaTime);
         }
     }
 
     VRCPlayerApi GetNearestPlayer()
     {
-        VRCPlayerApi nearestPlayer = null;
-        float closestDistance = float.MaxValue;
-
-        VRCPlayerApi[] players = new VRCPlayerApi[VRCPlayerApi.GetPlayerCount()];
+        int count = VRCPlayerApi.GetPlayerCount();
+        if (players.Length < count) players = new VRCPlayerApi[count];
         VRCPlayerApi.GetPlayers(players);
 
+        VRCPlayerApi nearestPlayer = null;
+        float closestDistance = float.MaxValue;
         Vector3 currentPosition = transform.position;
 
-        foreach (VRCPlayerApi player in players)
+        for (int i = 0; i < count; i++)
         {
-            if (player != null) // Allow local player for nearest search
+            VRCPlayerApi player = players[i];
+            if (!Utilities.IsValid(player)) continue; // The local player counts too
+
+            float distance = Vector3.Distance(currentPosition, player.GetPosition());
+            if (distance < closestDistance)
             {
-                float distance = Vector3.Distance(currentPosition, player.GetPosition());
-                if (distance < closestDistance)
-                {
-                    closestDistance = distance;
-                    nearestPlayer = player;
-                }
+                closestDistance = distance;
+                nearestPlayer = player;
             }
         }
 

+ 9 - 7
Runtime/Checkpoint.cs

@@ -1,26 +1,28 @@
-using UdonSharp;
+using UdonSharp;
 using UnityEngine;
 using VRC.SDKBase;
-using VRC.Udon;
-using System;
 
+[UdonBehaviourSyncMode(BehaviourSyncMode.NoVariableSync)]
 [DisallowMultipleComponent]
-
 [AddComponentMenu("Artyom Scripting System/Checkpoint")]
 public class Checkpoint : UdonSharpBehaviour
 {
     public GameObject targetObject; // The game object to move
 
-    [UdonSynced]
+    // Each player has their own progress, so this stays local: syncing it let one player's checkpoint block everyone's
     private bool hasBeenUsed = false;
 
-
     public override void OnPlayerTriggerEnter(VRCPlayerApi player)
     {
         if (hasBeenUsed || !player.isLocal) return;
+        if (targetObject == null)
+        {
+            Debug.LogError("[AUMSS] Checkpoint: Target Object isn't assigned.");
+            return;
+        }
 
         targetObject.transform.SetPositionAndRotation(transform.position, transform.rotation);
         hasBeenUsed = true;
-        gameObject.SetActive(false); // Disables the script after use
+        gameObject.SetActive(false); // Hides the checkpoint for this player once used
     }
 }

+ 11 - 4
Runtime/ImpactSoundPlayer.cs

@@ -1,9 +1,7 @@
-using UdonSharp;
+using UdonSharp;
 using UnityEngine;
-using VRC.SDKBase;
-using VRC.Udon;
-using System;
 
+[UdonBehaviourSyncMode(BehaviourSyncMode.NoVariableSync)]
 [DisallowMultipleComponent]
 [AddComponentMenu("Artyom Scripting System/Impact Sound Player")]
 public class ImpactSoundPlayer : UdonSharpBehaviour
@@ -15,9 +13,18 @@ public class ImpactSoundPlayer : UdonSharpBehaviour
     public float mediumImpactThreshold = 5.0f; // Threshold for medium impact
     public float largeImpactThreshold = 10.0f; // Threshold for large impact
 
+    [Tooltip("Impacts slower than this play nothing. 0 plays a sound for every contact, however gentle.")]
+    public float minimumImpact = 0f;
+    [Tooltip("Seconds after a sound before the next can play. 0 means no limit.")]
+    public float cooldown = 0f;
+
+    private float nextSoundTime;
+
     private void OnCollisionEnter(Collision collision)
     {
         float impactForce = collision.relativeVelocity.magnitude;
+        if (impactForce < minimumImpact || Time.time < nextSoundTime) return;
+        nextSoundTime = Time.time + cooldown;
 
         if (impactForce >= largeImpactThreshold)
         {

+ 2 - 2
Runtime/InteractScript.cs

@@ -1,11 +1,11 @@
-using UdonSharp;
+using UdonSharp;
 using UnityEngine;
 using VRC.SDKBase;
 using VRC.Udon;
 using System;
 
 [Obsolete("Replaced with Linked Object System")]
-[UdonBehaviourSyncMode(BehaviourSyncMode.Manual)]
+[UdonBehaviourSyncMode(BehaviourSyncMode.NoVariableSync)]
 [DisallowMultipleComponent]
 
 [AddComponentMenu("Artyom Scripting System/_DEPRECATED_/Interact Script")]

+ 65 - 35
Runtime/JumpPad.cs

@@ -1,14 +1,10 @@
-
-using System.Numerics;
 using UdonSharp;
 using UnityEngine;
 using VRC.SDK3.Components;
 using VRC.SDKBase;
-using VRC.Udon;
-using Vector3 = UnityEngine.Vector3;
 
+[UdonBehaviourSyncMode(BehaviourSyncMode.NoVariableSync)]
 [DisallowMultipleComponent]
-
 [AddComponentMenu("Artyom Scripting System/Jump Pad Controller")]
 public class JumpPad : UdonSharpBehaviour
 {
@@ -28,15 +24,24 @@ public class JumpPad : UdonSharpBehaviour
     private float defaultRun;
 
     private VRCPlayerApi player;
-
+    private bool ready;
+    private int localLaunches; // Launches of the local player still in the air
+    private int animLaunches; // Launches of anyone still holding the launch animation on
 
     void Start()
     {
-        launchAnim = GetComponent<Animator>();
-        appliedVelocity = CalculateInitialVelocity(self.position, jumpTarget.position, -9.8f, arcTime);
-        if (debug) Debug.Log("Jump Pad Controller: Velocity Calculated to:"+ appliedVelocity.ToString()); 
+        // The inspector's Animator wins; only fall back to one on this object
+        if (launchAnim == null) launchAnim = GetComponent<Animator>();
+        if (self == null) self = transform;
+        if (jumpTarget == null)
+        {
+            Debug.LogError("[AUMSS] Jump Pad Controller: Jump Target isn't assigned, so the pad won't launch anything.");
+            return;
+        }
+        ready = true;
+        if (debug) Debug.Log("Jump Pad Controller: Velocity Calculated to:" + CalculateInitialVelocity(self.position, jumpTarget.position, Physics.gravity.y, arcTime).ToString());
     }
-    
+
     public static Vector3 CalculateInitialVelocity(Vector3 origin, Vector3 target, float gravity, float time)
     {
         // Compute the differences in position
@@ -52,60 +57,85 @@ public class JumpPad : UdonSharpBehaviour
         return initialVelocity;
     }
 
-    public override void OnPlayerTriggerEnter(VRCPlayerApi colidedPlayer) {
-        appliedVelocity = CalculateInitialVelocity(self.position, jumpTarget.position, -9.8f, arcTime);
-        launchAnim.SetBool(animName, true);
-        if (debug) Debug.Log("Jump Pad Controller: Animator State attempted to be set to True");
+    private float LaunchDuration()
+    {
+        return Mathf.Max(arcTime - 0.2f, 0f);
+    }
+
+    public override void OnPlayerTriggerEnter(VRCPlayerApi collidedPlayer)
+    {
+        if (!ready) return;
+        StartLaunchAnim();
+        SendCustomEventDelayedSeconds(nameof(EndLaunchAnim), LaunchDuration());
         // Remote players get launched by their own client, and speed/velocity calls on them throw and halt this script
-        if (!colidedPlayer.isLocal)
+        if (!collidedPlayer.isLocal) return;
+
+        player = collidedPlayer;
+        // Only save the speeds when not already mid-launch: mid-launch they're the limits, and would be kept forever
+        if (localLaunches == 0)
         {
-            SendCustomEventDelayedSeconds(nameof(EndLaunchAnim), arcTime-0.2f);
-            return;
+            defaultRun = player.GetRunSpeed();
+            defaultStrafe = player.GetStrafeSpeed();
+            defaultWalk = player.GetWalkSpeed();
         }
-        player = colidedPlayer;
-        // Get speeds to reinstate after jump imobilization
-        defaultRun = player.GetRunSpeed();
-        defaultStrafe = player.GetStrafeSpeed();
-        defaultWalk = player.GetWalkSpeed();
-        // Set current velocity to triggered player and limit their movement
+        localLaunches++;
+
+        // Players fall with the world's gravity scaled by their own gravity strength
+        appliedVelocity = CalculateInitialVelocity(self.position, jumpTarget.position, Physics.gravity.y * player.GetGravityStrength(), arcTime);
         LimitMobility();
         player.SetVelocity(appliedVelocity);
-        
-        if (debug) Debug.Log("Jump Pad Controller: Player Velocity Set to:"+ appliedVelocity.ToString());
-        SendCustomEventDelayedSeconds(nameof(Mobilize), arcTime-0.2f);
-        
+
+        if (debug) Debug.Log("Jump Pad Controller: Player Velocity Set to:" + appliedVelocity.ToString());
+        SendCustomEventDelayedSeconds(nameof(Mobilize), LaunchDuration());
     }
 
     private void OnTriggerEnter(Collider other)
     {
-        appliedVelocity = CalculateInitialVelocity(self.position, jumpTarget.position, -9.8f, arcTime);
+        if (!ready || other == null) return;
         VRC_Pickup pickup = other.GetComponent<VRC_Pickup>();
         Rigidbody body = other.GetComponent<Rigidbody>();
-        if (pickup && body)
-        {
-            pickup.Drop();
-            body.velocity = appliedVelocity;
-        }
+        if (pickup == null || body == null) return;
+        // A synced pickup is launched by its owner; anyone else setting its velocity just fights the sync
+        if (other.GetComponent<VRCObjectSync>() != null && !Networking.IsOwner(other.gameObject)) return;
+
+        appliedVelocity = CalculateInitialVelocity(self.position, jumpTarget.position, Physics.gravity.y, arcTime);
+        pickup.Drop();
+        body.velocity = appliedVelocity;
     }
 
     public void LimitMobility()
     {
+        if (!Utilities.IsValid(player)) return;
         player.SetWalkSpeed(walkLimit);
         player.SetRunSpeed(runLimit);
         player.SetStrafeSpeed(strafeLimit);
         if (debug) Debug.Log("Jump Pad Controller: Limited Player Mobility for " + arcTime.ToString() + " seconds");
     }
+
     public void Mobilize()
     {
+        if (localLaunches > 0) localLaunches--;
+        if (localLaunches > 0 || !Utilities.IsValid(player)) return; // Still in the air from a later launch
         player.SetWalkSpeed(defaultWalk);
         player.SetRunSpeed(defaultRun);
         player.SetStrafeSpeed(defaultStrafe);
         if (debug) Debug.Log("Jump Pad Controller: De-Limited Player Mobility after" + arcTime.ToString() + " seconds");
-        EndLaunchAnim();
     }
+
+    private void StartLaunchAnim()
+    {
+        animLaunches++;
+        if (launchAnim == null || string.IsNullOrEmpty(animName)) return;
+        launchAnim.SetBool(animName, true);
+        if (debug) Debug.Log("Jump Pad Controller: Animator State attempted to be set to True");
+    }
+
     public void EndLaunchAnim()
     {
-        launchAnim.SetBool(animName,false);
+        if (animLaunches > 0) animLaunches--;
+        if (animLaunches > 0) return; // Someone else launched since; their launch ends it
+        if (launchAnim == null || string.IsNullOrEmpty(animName)) return;
+        launchAnim.SetBool(animName, false);
         if (debug) Debug.Log("Jump Pad Controller: Animator State attempted to be set to False");
     }
 }

+ 16 - 12
Runtime/LaserBeam.cs

@@ -1,11 +1,8 @@
-using UdonSharp;
+using UdonSharp;
 using UnityEngine;
-using VRC.SDKBase;
-using VRC.Udon;
-using System;
 
+[UdonBehaviourSyncMode(BehaviourSyncMode.NoVariableSync)]
 [DisallowMultipleComponent]
-
 [AddComponentMenu("Artyom Scripting System/Laser Beam Controller")]
 public class LaserBeam : UdonSharpBehaviour
 {
@@ -14,6 +11,16 @@ public class LaserBeam : UdonSharpBehaviour
     public float maxDistance = 100.0f; // Maximum distance the laser can reach
     public LayerMask layerMask; // Layer mask to specify which colliders to detect
     public GameObject endPointObject; // GameObject to place at the end of the laser line
+    [Tooltip("Whether the beam stops at trigger colliders, such as a Laser Receiver's trigger.")]
+    public bool hitTriggers = true;
+
+    void Start()
+    {
+        // World-space points draw correctly whichever object the Line Renderer is on
+        if (lineRenderer != null) lineRenderer.useWorldSpace = true;
+        if (layerMask.value == 0)
+            Debug.LogWarning("[AUMSS] Laser Beam Controller: Layer Mask is Nothing, so the beam passes through everything.");
+    }
 
     void Update()
     {
@@ -24,18 +31,15 @@ public class LaserBeam : UdonSharpBehaviour
 
         RaycastHit hit;
         Vector3 laserEndPoint = laserOrigin.position + (laserOrigin.forward * maxDistance);
+        QueryTriggerInteraction triggers = hitTriggers ? QueryTriggerInteraction.Collide : QueryTriggerInteraction.Ignore;
 
-        if (Physics.Raycast(laserOrigin.position, laserOrigin.forward, out hit, maxDistance, layerMask))
+        if (Physics.Raycast(laserOrigin.position, laserOrigin.forward, out hit, maxDistance, layerMask.value, triggers))
         {
             laserEndPoint = hit.point;
         }
 
-        // Convert world positions to local positions relative to the laser object's transform
-        Vector3 localLaserOrigin = transform.InverseTransformPoint(laserOrigin.position);
-        Vector3 localLaserEndPoint = transform.InverseTransformPoint(laserEndPoint);
-
-        lineRenderer.SetPosition(0, localLaserOrigin);
-        lineRenderer.SetPosition(1, localLaserEndPoint);
+        lineRenderer.SetPosition(0, laserOrigin.position);
+        lineRenderer.SetPosition(1, laserEndPoint);
 
         // Place the endPointObject at the end of the laser line
         if (endPointObject != null)

+ 51 - 33
Runtime/LaserReceiver.cs

@@ -1,11 +1,9 @@
-using UdonSharp;
+using UdonSharp;
 using UnityEngine;
 using VRC.SDKBase;
-using VRC.Udon;
-using System;
 
+[UdonBehaviourSyncMode(BehaviourSyncMode.NoVariableSync)]
 [DisallowMultipleComponent]
-
 [AddComponentMenu("Artyom Scripting System/Laser Receiver Controller")]
 public class LaserReceiver : UdonSharpBehaviour
 {
@@ -16,17 +14,30 @@ public class LaserReceiver : UdonSharpBehaviour
     public GameObject audioObjectTrue; // GameObject to enable when the state changes to true
     public GameObject audioObjectFalse; // GameObject to enable when the state changes to false
     public GameObject audioObjectLoop; // GameObject to loop while the state is true
-    public Collider triggerCollider; // Reference to the trigger collider
+    public Collider triggerCollider; // Reference to the trigger collider (this object's collider if empty)
     public Renderer targetRenderer; // Renderer to change material
     public Material materialTrue; // Material to apply when the state is true
     public Material materialFalse; // Material to apply when the state is false
     public int materialIndex = 0; // Index of the material to change
+    [Tooltip("Log each change of the number of lasers inside")]
+    public bool debugMode;
 
     private int colliderCount = 0; // Counter for colliders with the laser layer
-    private Collider[] collidersInsideTrigger;
+    private Collider[] collidersInsideTrigger = new Collider[16]; // Reused every frame
+    private BoxCollider box; // Set when the trigger is a box: checked exactly, rotation included
 
     private void Start()
     {
+        if (triggerCollider == null) triggerCollider = GetComponent<Collider>();
+        if (triggerCollider == null)
+        {
+            Debug.LogError("[AUMSS] Laser Receiver Controller: no Trigger Collider assigned, and none on this object.");
+            enabled = false; // Stops Update, which would otherwise throw and halt the script
+            return;
+        }
+        box = triggerCollider.GetComponent<BoxCollider>();
+        if (box != triggerCollider) box = null; // Another collider on the same object: fall back to the bounds
+
         // Ensure the target object is initially off
         if (targetObject != null)
         {
@@ -45,25 +56,40 @@ public class LaserReceiver : UdonSharpBehaviour
 
     private void Update()
     {
+        if (triggerCollider == null) return; // Not set up (enabled = false only takes effect next frame)
         // Continuously check for colliders inside the trigger
         UpdateColliderCount();
     }
 
     private void UpdateColliderCount()
     {
-        // Define the size and position of the box for the OverlapBox check
-        Vector3 boxCenter = triggerCollider.bounds.center;
-        Vector3 boxHalfExtents = triggerCollider.bounds.extents;
+        Vector3 boxCenter;
+        Vector3 boxHalfExtents;
+        Quaternion boxRotation;
+        if (box != null)
+        {
+            Transform boxTransform = box.transform;
+            boxCenter = boxTransform.TransformPoint(box.center);
+            boxHalfExtents = Vector3.Scale(box.size, boxTransform.lossyScale) * 0.5f;
+            boxHalfExtents = new Vector3(Mathf.Abs(boxHalfExtents.x), Mathf.Abs(boxHalfExtents.y), Mathf.Abs(boxHalfExtents.z));
+            boxRotation = boxTransform.rotation;
+        }
+        else
+        {
+            // Other collider shapes: the world-space box around them
+            boxCenter = triggerCollider.bounds.center;
+            boxHalfExtents = triggerCollider.bounds.extents;
+            boxRotation = Quaternion.identity;
+        }
 
-        // Perform the OverlapBox check
-        collidersInsideTrigger = Physics.OverlapBox(boxCenter, boxHalfExtents, Quaternion.identity, 1 << laserLayer);
+        int found = Physics.OverlapBoxNonAlloc(boxCenter, boxHalfExtents, collidersInsideTrigger, boxRotation,
+            1 << laserLayer, QueryTriggerInteraction.UseGlobal);
 
         // Update the collider count based on the result
         int newColliderCount = 0;
-
-        foreach (var col in collidersInsideTrigger)
+        for (int i = 0; i < found; i++)
         {
-            if (col.GetComponent<VRC_Pickup>() == null)
+            if (collidersInsideTrigger[i].GetComponent<VRC_Pickup>() == null)
             {
                 newColliderCount++;
             }
@@ -72,33 +98,24 @@ public class LaserReceiver : UdonSharpBehaviour
         if (newColliderCount != colliderCount)
         {
             colliderCount = newColliderCount;
-            Debug.Log("Updated collider count: " + colliderCount);
+            if (debugMode) Debug.Log("[AUMSS] Laser Receiver Controller: lasers inside: " + colliderCount);
 
             // Turn on or off the target object based on the collider count
-            if (colliderCount > 0)
-            {
-                if (targetObject != null)
-                {
-                    targetObject.SetActive(true);
-                }
-                UpdateAnims(true);
-            }
-            else if (colliderCount == 0)
+            bool on = colliderCount > 0;
+            if (targetObject != null)
             {
-                if (targetObject != null)
-                {
-                    targetObject.SetActive(false);
-                }
-                UpdateAnims(false);
+                targetObject.SetActive(on);
             }
+            UpdateAnims(on);
         }
     }
 
     private void UpdateAnims(bool state)
     {
-        for (int i = 0; i < animators.Length; i++)
+        int count = Mathf.Min(animators.Length, parameterNames.Length);
+        for (int i = 0; i < count; i++)
         {
-            if (animators[i] != null)
+            if (animators[i] != null && !string.IsNullOrEmpty(parameterNames[i]))
             {
                 animators[i].SetBool(parameterNames[i], state);
             }
@@ -123,11 +140,12 @@ public class LaserReceiver : UdonSharpBehaviour
 
     private void SetMaterial(Material material)
     {
-        Material[] materials = targetRenderer.materials;
+        // sharedMaterials: .materials would make a new copy of every material on each change
+        Material[] materials = targetRenderer.sharedMaterials;
         if (materialIndex >= 0 && materialIndex < materials.Length)
         {
             materials[materialIndex] = material;
-            targetRenderer.materials = materials;
+            targetRenderer.sharedMaterials = materials;
         }
     }
 }

+ 8 - 10
Runtime/LinkedObjectSystem.cs

@@ -1,8 +1,6 @@
-using UdonSharp;
+using UdonSharp;
 using UnityEngine;
-using UnityEngine.Rendering;
 using VRC.SDKBase;
-using VRC.Udon;
 
 [DisallowMultipleComponent]
 [AddComponentMenu("Artyom Scripting System/Linked Object System")]
@@ -11,7 +9,7 @@ public class LinkedObjectSystem : UdonSharpBehaviour
     [Header("Script Settings\n")]
     [Tooltip("Print all variables to console for debug purposes")]
     public bool debugMode;
-    [Tooltip("Delay for Temporarty interact, ignore if not needed")]
+    [Tooltip("Delay for Temporary interact, ignore if not needed")]
     public float delayedInteractTime = 0f;
 
     public LayerMask excludeLayers;
@@ -85,9 +83,11 @@ public class LinkedObjectSystem : UdonSharpBehaviour
       
     private void UpdateAnims(bool state)
     {
-        for (int i = 0; i < outputAnimators.Length; i++)
+        int count = Mathf.Min(outputAnimators.Length, outputParameterNames.Length);
+        for (int i = 0; i < count; i++)
         {
-            outputAnimators[i].SetBool(outputParameterNames[i], state);
+            if (outputAnimators[i] != null && !string.IsNullOrEmpty(outputParameterNames[i]))
+                outputAnimators[i].SetBool(outputParameterNames[i], state);
         }
 
         // Enable or disable GameObjects based on the state
@@ -179,8 +179,7 @@ public class LinkedObjectSystem : UdonSharpBehaviour
                 isOccupied = 1;
                 if (debugMode) Debug.LogWarning("LOS: Logged isOccupied as " + isOccupied.ToString());
                 UpdateAnims(true);
-                UdonBehaviour behaviour = (UdonBehaviour)GetComponent(typeof(UdonBehaviour));
-                behaviour.DisableInteractive = true;
+                DisableInteractive = true;
                 SendCustomEventDelayedSeconds(nameof(DisableTemp), delayedInteractTime+0.2f);
             }
         }
@@ -198,8 +197,7 @@ public class LinkedObjectSystem : UdonSharpBehaviour
     }
     public void DisableTemp()
     {
-        UdonBehaviour behaviour = (UdonBehaviour)GetComponent(typeof(UdonBehaviour));
-        behaviour.DisableInteractive = false;
+        DisableInteractive = false;
         isOccupied = 0;
         if (debugMode) Debug.LogWarning("LOS: Logged isOccupied as " + isOccupied.ToString());
     }

+ 2 - 2
Runtime/PickupDetector.cs

@@ -1,11 +1,11 @@
-using UdonSharp;
+using UdonSharp;
 using UnityEngine;
 using VRC.SDKBase;
 using VRC.Udon;
 using System;
 
 [Obsolete("Replaced with Linked Object System")]
-[UdonBehaviourSyncMode(BehaviourSyncMode.Manual)]
+[UdonBehaviourSyncMode(BehaviourSyncMode.NoVariableSync)]
 [DisallowMultipleComponent]
 
 [AddComponentMenu("Artyom Scripting System/_DEPRECATED_/Pickup Detector")]

+ 108 - 103
Runtime/PickupFizzler.cs

@@ -1,19 +1,17 @@
-using UdonSharp;
+using UdonSharp;
 using UnityEngine;
 using VRC.SDKBase;
 using VRC.SDK3.Components;
-using VRC.Udon;
 using System;
 [Obsolete("Component under review for further dubugging... Pardon our dust!")]
 
+[UdonBehaviourSyncMode(BehaviourSyncMode.NoVariableSync)]
 [DisallowMultipleComponent]
-
 [AddComponentMenu("Artyom Scripting System/Pickup Fizzler")]
 public class PickupFizzler : UdonSharpBehaviour
 {
     public AudioSource fizzlerSound; // Sound to play when a pickup is fizzled
     public AudioSource playerEnterSound; // Sound to play when a player enters the collider
-    public int fizzlerLayer = 8; // Layer number for the fizzler
     public int pickupLayer = 13; // Layer number for the pickup (VRChat's Pickup layer)
     public float fizzleDuration = 2.0f; // Duration for the fizzle effect
     public Material fizzleMaterial; // Material to apply during the fizzle effect
@@ -27,6 +25,9 @@ public class PickupFizzler : UdonSharpBehaviour
     private float fizzleTimer = 0.0f;
     private Vector3 storedVelocity;
     private Vector3 storedAngularVelocity;
+    private Transform soundParent; // Where the fizzle sound goes back to after following a pickup
+    private Vector3 soundLocalPosition;
+    private Quaternion soundLocalRotation;
 
     private void Start()
     {
@@ -34,6 +35,9 @@ public class PickupFizzler : UdonSharpBehaviour
         if (fizzlerSound != null)
         {
             fizzlerSound.Stop();
+            soundParent = fizzlerSound.transform.parent;
+            soundLocalPosition = fizzlerSound.transform.localPosition;
+            soundLocalRotation = fizzlerSound.transform.localRotation;
         }
 
         if (playerEnterSound != null)
@@ -45,72 +49,65 @@ public class PickupFizzler : UdonSharpBehaviour
     private void OnTriggerEnter(Collider other)
     {
         // Check if the collider is on the pickup layer and not already fizzling
-        if (other.gameObject.layer == pickupLayer && !isFizzling)
+        if (other == null || other.gameObject.layer != pickupLayer || isFizzling) return;
+
+        // Check if the collider has a VRC_Pickup component
+        pickup = other.GetComponent<VRC_Pickup>();
+        if (pickup == null) return;
+
+        // Get the VRCObjectSync component
+        objectSync = pickup.GetComponent<VRCObjectSync>();
+        if (objectSync == null)
+        {
+            Debug.LogError("[AUMSS] Pickup Fizzler: no VRCObjectSync on " + pickup.gameObject.name + ", so it can't be respawned.");
+            return;
+        }
+
+        // Get the pickup's collider and rigidbody
+        pickupCollider = pickup.GetComponent<Collider>();
+        pickupRigidbody = pickup.GetComponent<Rigidbody>();
+
+        // Store the current velocity and angular velocity
+        if (pickupRigidbody != null)
+        {
+            storedVelocity = pickupRigidbody.velocity;
+            storedAngularVelocity = pickupRigidbody.angularVelocity;
+        }
+
+        // Swap every material for the fizzle material. sharedMaterials: .materials would copy them all each time
+        renderers = pickup.GetComponentsInChildren<Renderer>();
+        originalMaterials = new Material[renderers.Length][];
+        for (int i = 0; i < renderers.Length; i++)
         {
-            // Check if the collider has a VRC_Pickup component
-            pickup = other.GetComponent<VRC_Pickup>();
-            if (pickup != null)
+            originalMaterials[i] = renderers[i].sharedMaterials;
+            Material[] newMaterials = new Material[originalMaterials[i].Length];
+            for (int j = 0; j < newMaterials.Length; j++)
             {
-                // Get the VRCObjectSync component
-                objectSync = pickup.GetComponent<VRCObjectSync>();
-                if (objectSync == null)
-                {
-                    Debug.LogError("No VRCObjectSync component found on the pickup object.");
-                    return;
-                }
-                UdonBehaviour behaviour = (UdonBehaviour)GetComponent(typeof(UdonBehaviour));
-                behaviour.DisableInteractive = true;
-
-                // Get the pickup's collider and rigidbody
-                pickupCollider = pickup.GetComponent<Collider>();
-                pickupRigidbody = pickup.GetComponent<Rigidbody>();
-
-                // Store the current velocity and angular velocity
-                if (pickupRigidbody != null)
-                {
-                    storedVelocity = pickupRigidbody.velocity;
-                    storedAngularVelocity = pickupRigidbody.angularVelocity;
-                }
-
-                // Get the renderers and materials of the pickup
-                renderers = pickup.GetComponentsInChildren<Renderer>();
-                originalMaterials = new Material[renderers.Length][];
-                for (int i = 0; i < renderers.Length; i++)
-                {
-                    originalMaterials[i] = renderers[i].materials;
-                    Material[] newMaterials = new Material[renderers[i].materials.Length];
-                    for (int j = 0; j < newMaterials.Length; j++)
-                    {
-                        newMaterials[j] = fizzleMaterial;
-                    }
-                    renderers[i].materials = newMaterials;
-                }
-
-                // Play the fizzler sound
-                if (fizzlerSound != null)
-                {
-                    fizzlerSound.transform.SetParent(pickup.transform);
-                    fizzlerSound.transform.localPosition = Vector3.zero;
-                    fizzlerSound.Play();
-                }
-
-                // Disable the pickup's collider
-                if (pickupCollider != null)
-                {
-                    pickupCollider.enabled = false;
-                }
-
-                // Force the player to drop the pickup
-                if (Networking.LocalPlayer != null)
-                {
-                    pickup.Drop(Networking.LocalPlayer);
-                }
-
-                // Start the fizzling process
-                isFizzling = true;
-                fizzleTimer = fizzleDuration;
+                newMaterials[j] = fizzleMaterial;
             }
+            renderers[i].sharedMaterials = newMaterials;
+        }
+
+        // Play the fizzler sound
+        if (fizzlerSound != null)
+        {
+            fizzlerSound.transform.SetParent(pickup.transform);
+            fizzlerSound.transform.localPosition = Vector3.zero;
+            fizzlerSound.Play();
+        }
+
+        // Disable the pickup's collider
+        if (pickupCollider != null)
+        {
+            pickupCollider.enabled = false;
         }
+
+        // Force the player to drop the pickup
+        pickup.Drop();
+
+        // Start the fizzling process
+        isFizzling = true;
+        fizzleTimer = fizzleDuration;
     }
 
     public override void OnPlayerTriggerEnter(VRCPlayerApi player)
@@ -123,52 +120,60 @@ public class PickupFizzler : UdonSharpBehaviour
 
     private void Update()
     {
-        if (isFizzling)
+        if (!isFizzling) return;
+
+        if (!Utilities.IsValid(pickup))
+        {
+            // The pickup was destroyed mid-fizzle: put the sound back and wait for the next one
+            ResetSound();
+            isFizzling = false;
+            return;
+        }
+
+        fizzleTimer -= Time.deltaTime;
+        // Only the owner moves and respawns it; anyone else would fight the sync
+        bool isOwner = Networking.IsOwner(pickup.gameObject);
+
+        // Apply the stored velocity and angular velocity
+        if (isOwner && pickupRigidbody != null)
         {
-            fizzleTimer -= Time.deltaTime;
+            pickupRigidbody.velocity = storedVelocity;
+            pickupRigidbody.angularVelocity = storedAngularVelocity;
+        }
+        pickup.Drop();
 
-            // Apply the stored velocity and angular velocity
-            if (pickupRigidbody != null)
+        if (fizzleTimer <= 0.0f)
+        {
+            // Respawn the pickup using VRCObjectSync
+            if (isOwner && objectSync != null)
             {
-                pickupRigidbody.velocity = storedVelocity;
-                pickupRigidbody.angularVelocity = storedAngularVelocity;
+                objectSync.Respawn();
             }
-            if (Networking.LocalPlayer != null)
+
+            ResetSound();
+
+            // Re-enable the pickup's collider
+            if (pickupCollider != null)
             {
-                pickup.Drop(Networking.LocalPlayer);
+                pickupCollider.enabled = true;
             }
 
-            if (fizzleTimer <= 0.0f)
+            // Reset the original materials
+            for (int i = 0; i < renderers.Length; i++)
             {
-                // Respawn the pickup using VRCObjectSync
-                if (objectSync != null)
-                {
-                    objectSync.Respawn();
-                }
-
-                // Stop and detach the sound
-                if (fizzlerSound != null)
-                {
-                    fizzlerSound.Stop();
-                    fizzlerSound.transform.SetParent(null);
-                }
-
-                // Re-enable the pickup's collider
-                if (pickupCollider != null)
-                {
-                    pickupCollider.enabled = true;
-                }
-
-                // Reset the original materials
-                for (int i = 0; i < renderers.Length; i++)
-                {
-                    renderers[i].materials = originalMaterials[i];
-                }
-
-                isFizzling = false;
-                UdonBehaviour behaviour = (UdonBehaviour)GetComponent(typeof(UdonBehaviour));
-                behaviour.DisableInteractive = false;
+                if (renderers[i] != null) renderers[i].sharedMaterials = originalMaterials[i];
             }
+
+            isFizzling = false;
         }
     }
+
+    private void ResetSound()
+    {
+        if (fizzlerSound == null) return;
+        fizzlerSound.Stop();
+        fizzlerSound.transform.SetParent(soundParent);
+        fizzlerSound.transform.localPosition = soundLocalPosition;
+        fizzlerSound.transform.localRotation = soundLocalRotation;
+    }
 }

+ 33 - 37
Runtime/PickupMovementFixer.cs

@@ -1,11 +1,10 @@
-using UdonSharp;
+using UdonSharp;
 using UnityEngine;
+using VRC.SDK3.Components;
 using VRC.SDKBase;
-using VRC.Udon;
-using System;
 
+[UdonBehaviourSyncMode(BehaviourSyncMode.NoVariableSync)]
 [DisallowMultipleComponent]
-
 [AddComponentMenu("Artyom Scripting System/Pickup Following Mover Fix")]
 public class PickupMovementFixer : UdonSharpBehaviour
 {
@@ -15,67 +14,64 @@ public class PickupMovementFixer : UdonSharpBehaviour
     private Collider triggerCollider;
     private Vector3 previousTargetPosition;
     private Vector3 targetVelocity;
+    private bool carrying; // While the target moves, and for one frame after it stops
+    private bool ready;
 
     private void Start()
     {
         triggerCollider = GetComponent<Collider>();
         if (triggerCollider == null || !triggerCollider.isTrigger)
         {
-            Debug.LogError("PickupMovementFixer: Collider must be set as a trigger.");
+            Debug.LogError("[AUMSS] Pickup Following Mover Fix: Collider must be set as a trigger.");
+        }
+        if (targetObject == null)
+        {
+            Debug.LogError("[AUMSS] Pickup Following Mover Fix: Target Object isn't assigned.");
+            enabled = false;
+            return;
         }
         previousTargetPosition = targetObject.transform.position;
+        ready = true;
     }
 
     // LateUpdate runs after Update scripts and Animators have moved the target this frame,
     // so the movement measured here matches this frame's deltaTime
     private void LateUpdate()
     {
+        if (!ready) return; // Not set up (enabled = false only takes effect next frame)
         Vector3 currentTargetPosition = targetObject.transform.position;
-        targetVelocity = (currentTargetPosition - previousTargetPosition) / Time.deltaTime;
+        Vector3 newVelocity = (currentTargetPosition - previousTargetPosition) / Time.deltaTime;
         previousTargetPosition = currentTargetPosition;
+
+        // A still target leaves the pickups to their own physics, so they can fall and slide.
+        // The frame it stops still applies (zero), so the pickups stop with it instead of sliding off.
+        bool moving = newVelocity.sqrMagnitude > 0.0001f;
+        carrying = moving || targetVelocity.sqrMagnitude > 0.0001f;
+        targetVelocity = newVelocity;
     }
 
     private void OnTriggerEnter(Collider other)
     {
-        if (other.gameObject.layer == pickupLayer)
-        {
-            VRC_Pickup pickup = other.gameObject.GetComponent<VRC_Pickup>();
-            if (pickup != null)
-            {
-                Vector3 relativePosition = other.transform.position - targetObject.transform.position;
-                pickup.gameObject.transform.position = targetObject.transform.position + relativePosition;
-                Rigidbody pickupRigidbody = other.GetComponent<Rigidbody>();
-                if (pickupRigidbody != null)
-                {
-                    pickupRigidbody.velocity = targetVelocity;
-                }
-            }
-        }
+        Carry(other);
     }
 
     private void OnTriggerStay(Collider other)
     {
-        if (other.gameObject.layer == pickupLayer)
-        {
-            VRC_Pickup pickup = other.gameObject.GetComponent<VRC_Pickup>();
-            if (pickup != null)
-            {
-                Vector3 relativePosition = other.transform.position - targetObject.transform.position;
-                pickup.gameObject.transform.position = targetObject.transform.position + relativePosition;
-                Rigidbody pickupRigidbody = other.GetComponent<Rigidbody>();
-                if (pickupRigidbody != null)
-                {
-                    pickupRigidbody.velocity = targetVelocity;
-                }
-            }
-        }
+        Carry(other);
     }
 
-    private void OnTriggerExit(Collider other)
+    private void Carry(Collider other)
     {
-        if (other.gameObject.layer == pickupLayer)
+        if (!ready || !carrying || other == null || other.gameObject.layer != pickupLayer) return;
+        VRC_Pickup pickup = other.GetComponent<VRC_Pickup>();
+        if (pickup == null || pickup.IsHeld) return;
+        // A synced pickup is moved by its owner; anyone else setting its velocity just fights the sync
+        if (other.GetComponent<VRCObjectSync>() != null && !Networking.IsOwner(other.gameObject)) return;
+
+        Rigidbody pickupRigidbody = other.GetComponent<Rigidbody>();
+        if (pickupRigidbody != null)
         {
-            // Optionally, you can add code here to handle when the pickup exits the trigger
+            pickupRigidbody.velocity = targetVelocity;
         }
     }
 }

+ 20 - 22
Runtime/PickupSpeedLimiter.cs

@@ -1,46 +1,44 @@
-using UdonSharp;
+using UdonSharp;
 using UnityEngine;
+using VRC.SDK3.Components;
 using VRC.SDKBase;
-using VRC.Udon;
-using System;
 
+[UdonBehaviourSyncMode(BehaviourSyncMode.NoVariableSync)]
 [DisallowMultipleComponent]
-
 [AddComponentMenu("Artyom Scripting System/Pickup Speed Limiter")]
-
 public class PickupSpeedLimiter : UdonSharpBehaviour
 {
     public float maxSpeed = 5.0f; // Maximum allowed speed for the pickup
     public bool debugSpeed;
     private VRC_Pickup pickup; // Reference to the VRC_Pickup component
+    private Rigidbody rb;
+    private bool synced; // Has VRCObjectSync: only its owner limits it
 
     void Start()
     {
         pickup = GetComponent<VRC_Pickup>();
+        rb = GetComponent<Rigidbody>();
+        synced = GetComponent<VRCObjectSync>() != null;
 
-        if (pickup == null)
+        if (pickup == null || rb == null)
         {
-            Debug.LogError("No VRC_Pickup component found on this GameObject.");
+            Debug.LogError("[AUMSS] Pickup Speed Limiter: needs a VRC_Pickup and a Rigidbody on this GameObject.");
+            enabled = false;
         }
     }
 
-    void Update()
+    // FixedUpdate: once per physics step, so no step runs over the limit
+    void FixedUpdate()
     {
-        if (pickup != null && !(pickup.IsHeld))
+        if (pickup == null || rb == null) return; // Not set up (enabled = false only takes effect next frame)
+        if (pickup.IsHeld) return;
+        if (synced && !Networking.IsOwner(gameObject)) return; // Anyone else would fight the sync
+
+        Vector3 velocity = rb.velocity;
+        if (velocity.sqrMagnitude > maxSpeed * maxSpeed)
         {
-            Rigidbody rb = pickup.GetComponent<Rigidbody>();
-            if (rb != null)
-            {
-                Vector3 velocity = rb.velocity;
-                
-                if (debugSpeed) {
-                    if (velocity.magnitude > maxSpeed) { Debug.LogWarning("Speed of cube: " + rb.velocity.magnitude.ToString());} 
-                    Debug.Log("Speed of cube: " + rb.velocity.magnitude.ToString()); }
-                if (velocity.magnitude > maxSpeed)
-                {
-                    rb.velocity = velocity.normalized * maxSpeed;
-                }
-            }
+            if (debugSpeed) Debug.LogWarning("[AUMSS] Pickup Speed Limiter: " + gameObject.name + " at " + velocity.magnitude.ToString() + ", limited to " + maxSpeed.ToString());
+            rb.velocity = velocity.normalized * maxSpeed;
         }
     }
 }

+ 3 - 2
Runtime/PlayerTrigger.cs

@@ -1,4 +1,4 @@
-
+
 using UdonSharp;
 using UnityEngine;
 using VRC.SDKBase;
@@ -6,9 +6,10 @@ using VRC.Udon;
 using System;
 
 [Obsolete("Replaced with Linked Object System")]
+[UdonBehaviourSyncMode(BehaviourSyncMode.NoVariableSync)]
 [DisallowMultipleComponent]
 
-[AddComponentMenu("Artyom Scripting System/Player Trigger")]
+[AddComponentMenu("Artyom Scripting System/_DEPRECATED_/Player Trigger")]
 public class PlayerTrigger : UdonSharpBehaviour
 {
     public Animator targetAnimator; // The Animator component to control

+ 21 - 10
Runtime/TimedTeleport.cs

@@ -1,10 +1,8 @@
-
-using System;
 using UdonSharp;
 using UnityEngine;
 using VRC.SDKBase;
-using VRC.Udon;
 
+[UdonBehaviourSyncMode(BehaviourSyncMode.NoVariableSync)]
 [AddComponentMenu("Artyom Scripting System/Timed Teleport")]
 [DisallowMultipleComponent]
 public class TimedTeleport : UdonSharpBehaviour
@@ -12,23 +10,36 @@ public class TimedTeleport : UdonSharpBehaviour
     [Header("Teleportation Settings")]
     public Transform teleportTarget; // The transform to teleport the player to
     public float delay = 1f; // The delay in seconds before teleportation
+    [Tooltip("Leaving the trigger before the delay is up cancels the teleport.")]
+    public bool cancelOnExit = false;
+
+    private bool pending; // A teleport is waiting: entering again doesn't queue another
 
     public override void OnPlayerTriggerEnter(VRCPlayerApi player)
     {
-        if (Networking.LocalPlayer == player)
+        if (!player.isLocal || pending) return;
+        if (teleportTarget == null)
         {
-            SendCustomEventDelayedSeconds(nameof(TeleportPlayer), delay);
+            Debug.LogError("[AUMSS] Timed Teleport: Teleport Target isn't assigned.");
+            return;
         }
-        
+        pending = true;
+        SendCustomEventDelayedSeconds(nameof(TeleportPlayer), delay);
+    }
+
+    public override void OnPlayerTriggerExit(VRCPlayerApi player)
+    {
+        if (cancelOnExit && player.isLocal) pending = false;
     }
 
     public void TeleportPlayer()
     {
-        Networking.LocalPlayer.TeleportTo(teleportTarget.position, 
-                                          teleportTarget.rotation, 
-                                          VRC_SceneDescriptor.SpawnOrientation.Default, 
+        if (!pending) return; // Cancelled by leaving the trigger
+        pending = false;
+        Networking.LocalPlayer.TeleportTo(teleportTarget.position,
+                                          teleportTarget.rotation,
+                                          VRC_SceneDescriptor.SpawnOrientation.Default,
                                           false
                                         );
     }
 }
-

+ 1 - 1
package.json

@@ -1,7 +1,7 @@
 {
     "name": "dev.av2x.aumss",
     "displayName": "AUMSS - Artyom's Udon Modular Scripting System",
-    "version": "1.0.1",
+    "version": "1.0.2",
     "unity": "2022.3",
     "description": "UdonSharp utility scripts for VRChat worlds: jump pads, checkpoints, lasers, pickup helpers, timed teleports and the Linked Object System.",
     "author": {