Synchronized Item Spawner Tutorial
This tutorial will guide you through creating a synchronized item spawner in Massive Loop. Interactable and grabbable items can be the cornerstone of your experience! Whether you are attempting to spawn interactive fruits and vegetables for that awesome cooking game you're working on, to simple soda cans, this script will give you a head start! Unlike single-player item spawning, networked objects require careful handling to ensure all players see the same state. We’ll cover the core concepts step by step, ensuring you understand not just how it works, but why each component is necessary.
Prerequisites
- Familiarity with Unity
- Massive Loop SDK installed
- A Massive Loop compatible Unity scene
Preface
Before diving into code, let’s clarify the key challenges:
- Synchronization – When one player spawns an object, all other players must see it instantly.
- Ownership – Who controls the object? The spawner? The user who clicked the spawner? Or the master client?
- Tracking – We need to clean up references when objects are destroyed. This ensures complete safety and helps secure efficiency
- Limits – Preventing players from spamming and spawning infinite objects.
Massive Loop provides tools like MLSynchronizer
and TransformSyncModule
to handle these issues, but we need to wire them up correctly.
Step 1: Setting Up the Spawner Object
First, we need an interactable object that players can click to spawn items.
- Create a new GameObject (e.g., a 3D cube) and name it
ItemSpawner
. - Add any collider of any type. Typically, I like to use box colliders or sphere colliders
- Add the
MLClickable
component – This enables player interactions. Without this, the spawner won’t respond to clicks.
At this point, clicking the object does nothing. We’ll add functionality next.
Step 2: Preparing the Spawnable Prefab
For an object to spawn correctly in multiplayer, it must be network-aware.
Critical Components:
MLSynchronizer
– Controls whether the object’s creation/destruction syncs across the network.- Enable:
Synchronize Instantiation & Destroy
(Ensures all clients see the object.)
- Enable:
TransformSyncModule
– Syncs position, rotation, and scale.- Save as a Prefab – Drag it into the
Resources
folder so it can be instantiated dynamically.
Why? Without these, objects might appear only for the spawning player, causing desyncs.
Step 3: Writing the Spawner Logic
Now, let’s write a script that:
- Listens for player clicks.
- Spawns objects in sync.
- Tracks and limits spawns.
Key Variables
This section defines the foundational elements the spawner needs to operate. The spawnablePrefab
holds the object we want to instantiate, which must be assigned in Unity's Inspector. The spawnLocation
determines where new objects appear in the game world, while maxSpawnedObjects
sets a cap to prevent excessive spawning. The spawnedObjects
list keeps track of all active instances, crucial for managing limits and cleanup. These variables work together to control what spawns, where it spawns, and how many can exist at once.
public GameObject spawnablePrefab; // Assign your prefab in Inspector
public Transform spawnLocation; // Where objects appear
public int maxSpawnedObjects = 10; // Prevent spamming
private List<GameObject> spawnedObjects = new List<GameObject>();
Handling Clicks
Here we connect player input to spawning logic. The Start
method fetches the MLClickable
component, which detects when players interact with the object. When a click occurs, OnSpawnButtonClicked
triggers, first checking if we haven't exceeded the spawn limit before proceeding. This ensures clicks only spawn objects when allowed, preventing players from flooding the game with items. The separation between input detection and spawning logic makes the system more modular and easier to modify later.
We’ll use MLClickable
to detect interactions:
private void Start() {
MLClickable clickable = GetComponent<MLClickable>();
clickable.OnPlayerClick.AddListener(OnSpawnButtonClicked);
}
private void OnSpawnButtonClicked(MLPlayer player) {
if (spawnedObjects.Count < maxSpawnedObjects) {
SpawnObject();
}
}
Networked Spawning
This is where the magic of synchronization happens. The SpawnObject
method first verifies the calling client has authority to spawn (master client only), preventing conflicts. It then creates the object at the designated location and immediately adds it to the tracking list. The TrackedObject
component gets attached to handle cleanup later - think of it as a tiny supervisor that reports back when its object gets destroyed. This careful orchestration ensures every client sees the same game state without objects mysteriously appearing or disappearing for some players.
To ensure all clients see the new object:
private void SpawnObject() {
if (!MassiveLoopClient.IsMasterClient) return; // Only master spawns
GameObject newObj = Instantiate(spawnablePrefab, spawnLocation.position, Quaternion.identity);
spawnedObjects.Add(newObj);
// Track destruction (we'll implement this next)
TrackedObject tracker = newObj.AddComponent<TrackedObject>();
tracker.parentSpawner = this;
}
Tracking Object Destruction
The TrackedObject
class solves a subtle but important problem: knowing when spawned objects get removed. Its OnDestroy
method acts like a death certificate, notifying the spawner when an object gets destroyed so the spawner can update its records. The parent reference creates a two-way communication channel - the spawner knows about the object, and the object can report back to the spawner. This prevents ghost entries in the tracking list and ensures our spawn count stays accurate, whether objects are removed manually or through gameplay systems.
When an object is destroyed (manually or automatically), we must update our list:
public class TrackedObject : MonoBehaviour {
public ItemSpawner parentSpawner;
private void OnDestroy() {
if (parentSpawner != null) {
parentSpawner.HandleObjectDestroyed(gameObject);
}
}
}
// Back in ItemSpawner.cs
public void HandleObjectDestroyed(GameObject obj) {
spawnedObjects.Remove(obj);
}
Step 4: Testing & Debugging
-
**Try locally building and running ** with multiple clients.
-
Click the spawner – Verify objects appear for everyone.
-
Move objects – Check if transforms sync smoothly. You may want to add the MLGrab component if you want users to pick up the item!
Final Thoughts
By now, you should have a fully functional synchronized spawner!
A very similar method is used inside of one of our immersive cooking games. Here is an example of a full script that uses these methods and design.
using ML.SDK;
using UnityEngine;
using System.Collections.Generic;
public class InstantiateObject : MonoBehaviour
{
// The axis of rotation (X or Y)
public enum RotationAxis { X_Axis, Y_Axis }
public RotationAxis rotationAxis = RotationAxis.Y_Axis;
// Rotation angles for open and closed positions
private Quaternion closedRotation;
private Quaternion openRotation;
public float rotationSpeed = 2f;
// Control door state
private bool isOpening = false;
private bool isClosing = false;
// Flag to track door state
private bool isDoorOpen = false;
public MLClickable clickableComponent;
public GameObject FoodObject;
public Transform spawnlocation;
const string EVENT_ID = "OnObjectInstantiate";
public int Count = 0;
const string EVENT_ID_CHANGEOBJNAME = "ChangeOBJName";
private GameObject newChicken;
public MLSyncVarAttribute MaxCount;
public int MaximumCount = 10;
// List to keep track of spawned objects
public List<GameObject> SpawnedObjects = new List<GameObject>();
public void OnPlayerClickFood(MLPlayer player)
{
Debug.Log("Local Button Pressed");
this.InvokeNetwork(EVENT_ID, EventTarget.All, null, true);
}
public void OnNetworkSpawnObject(object[] args)
{
if (this == null || gameObject == null || gameObject.name == null || args[0] == null)
{
return;
}
if (Count < MaximumCount)
{
SpawnObject();
}
}
public void OnNetworkChangeOBJname(object[] args)
{
if (this == null || gameObject == null || gameObject.name == null || args[0] == null)
{
return;
}
Debug.Log("Name change called from master client");
gameObject.name = (string)args[0];
Debug.Log("New name : " + gameObject.name);
}
EventToken InstantiateToken;
EventToken ChangeToken;
void Start()
{
clickableComponent.OnPlayerClick.AddListener(OnPlayerClickFood);
InstantiateToken = this.AddEventHandler(EVENT_ID, OnNetworkSpawnObject);
ChangeToken = this.AddEventHandler(EVENT_ID_CHANGEOBJNAME, OnNetworkChangeOBJname);
}
void Update()
{
// Update logic if needed
}
// Add this method to handle object removal
public void HandleObjectDestroyed(GameObject destroyedObject)
{
if (this == null || gameObject == null || gameObject.name == null )
{
return;
}
SpawnedObjects.Remove(destroyedObject);
Count -= 1;
}
// Modify SpawnObject() to set the reference to the parent script
public void SpawnObject()
{
Count += 1;
if (MassiveLoopClient.IsMasterClient)
{
newChicken = Object.Instantiate(FoodObject, spawnlocation.position, Quaternion.identity);
newChicken.name += Count;
this.InvokeNetwork(EVENT_ID_CHANGEOBJNAME, EventTarget.All, null, newChicken.name);
// Add the spawned object to the list
SpawnedObjects.Add(newChicken);
// Attach the TrackedObject script and set the parent script reference
TrackedObject trackedObj = (TrackedObject)newChicken.AddComponent(typeof(TrackedObject));
trackedObj.parentScript = this;
}
}
// Component for tracking object destruction
public class TrackedObject : MonoBehaviour
{
public InstantiateObject parentScript;
private void OnDestroy()
{
if (this == null || gameObject == null || gameObject.name == null)
{
return;
}
if (parentScript != null)
{
parentScript.HandleObjectDestroyed(this.gameObject);
}
}
}
}
Key takeaways:
- Networked objects require
MLSynchronizer
and atransformSyncmodule
– Without these, they won’t sync. - Track spawned objects – helps to minimize memory leaks and respect limits.
- Master client controls spawning – Prevents conflicts or multiple items from spawning.
For deeper customization, explore:
- Object pooling (for performance).
- Ownership transfer (letting players "claim" spawned items, this naturally happens when users grab an item with MLGrab).
- Custom properties (syncing health, color, etc.).
Now go forth and instantiate responsibly! 🚀