docs.unity3d.com
Search Results for

    Show / Hide Table of Contents

    Change Size Effect

    The Change Size Effect is a custom interaction effect that modifies the player's scale when they interact with an object. It demonstrates how to extend the Modular Interaction System to create networked gameplay mechanics.

    Overview

    • Smoothly resize the player (grow or shrink) upon interaction for puzzles (shrink to fit), power-ups (Giant mode).

    1. The Interface

    The IInteractionEffect interface requires the ApplyEffect method. This is where the logic begins.

    public class ChangeSizeEffect : NetworkBehaviour, IInteractionEffect
    {
        // ... Other interface methods
    
        public IEnumerator ApplyEffect(GameObject interactor, GameObject interactable)
        {
            // Calculate target size (grow or reset)
    
            // Trigger network synchronization
    
            // Wait for transition if we own the object
        }
    }
    

    2. Network Synchronization (RPC)

    Since visual changes must be seen by all players, we use a SendTo.Everyone Rpc to run the animation on every client.

    [Rpc(SendTo.Everyone)]
    private void TriggerSizeChangeRpc(ulong networkObjectId, Vector3 newTargetScale, bool isReset)
    {
        // Find the player object by ID
        // Stop any running animations
        // Store original data for resetting later
        // Start the animation coroutine
        // Play VFX and Sound
    }
    

    3. Full script

    using UnityEngine;
    using Unity.Netcode;
    using System.Collections;
    using Blocks.Gameplay.Core;
    
    /// <summary>
    /// Changes the scale of the interactor with smooth animation and optional physics adjustments.
    /// Fully synchronized across the network using RPCs.
    /// Great for creating shrink/grow power-ups or puzzle mechanics.
    /// </summary>
    public class ChangeSizeEffect : NetworkBehaviour, IInteractionEffect
    {
        [Header("Size Settings")]
        [SerializeField] private Vector3 targetScale = new Vector3(2f, 2f, 2f);
        [SerializeField] private float transitionDuration = 0.5f;
        [SerializeField] private AnimationCurve scaleCurve = AnimationCurve.EaseInOut(0, 0, 1, 1);
        [SerializeField] private bool resetOnNextInteraction = false;
    
        [Header("Effect Priority")]
        [SerializeField] private int priority = 0;
    
        [Header("Visual Effects")] 
        [SerializeField] private GameObject sizeChangeVFX;
        [SerializeField] private SoundDef sizeChangeSound;
    
        public int Priority => priority;
    
        private static readonly System.Collections.Generic.Dictionary<ulong, TransformData> k_OriginalData =
            new System.Collections.Generic.Dictionary<ulong, TransformData>();
    
        private static readonly System.Collections.Generic.Dictionary<ulong, Coroutine> k_ActiveCoroutines =
            new System.Collections.Generic.Dictionary<ulong, Coroutine>();
    
        [System.Serializable]
        private class TransformData
        {
            public Vector3 originalScale;
        }
    
        public IEnumerator ApplyEffect(GameObject interactor, GameObject interactable)
        {
            // Get NetworkObject to identify the player across the network
            if (!interactor.TryGetComponent<NetworkObject>(out var netObj))
            {
                Debug.LogWarning("[ChangeSizeEffect] Interactor does not have a NetworkObject component.", interactor);
                yield break;
            }
    
            ulong networkId = netObj.NetworkObjectId;
            bool shouldReset = resetOnNextInteraction && k_OriginalData.ContainsKey(networkId);
            Vector3 targetScaleValue = shouldReset ? k_OriginalData[networkId].originalScale : targetScale;
            TriggerSizeChangeRpc(networkId, targetScaleValue, shouldReset);
    
            if (netObj.IsOwner)
            {
                yield return new WaitForSeconds(transitionDuration);
            }
        }
    
        /// <summary>
        /// RPC that triggers the size change animation on all clients.
        /// </summary>
        [Rpc(SendTo.Everyone)]
        private void TriggerSizeChangeRpc(ulong networkObjectId, Vector3 newTargetScale, bool isReset)
        {
            // Find the NetworkObject by ID
            if (!NetworkManager.Singleton.SpawnManager.SpawnedObjects.TryGetValue(networkObjectId, out var netObj))
            {
                Debug.LogWarning($"[ChangeSizeEffect] Could not find NetworkObject with ID {networkObjectId}");
                return;
            }
    
            GameObject interactor = netObj.gameObject;
    
            // Stop any existing size change coroutine for this object
            if (k_ActiveCoroutines.TryGetValue(networkObjectId, out var existingCoroutine))
            {
                if (existingCoroutine != null)
                {
                    StopCoroutine(existingCoroutine);
                }
    
                k_ActiveCoroutines.Remove(networkObjectId);
            }
    
            // Store original data if not already stored
            if (!k_OriginalData.ContainsKey(networkObjectId))
            {
                k_OriginalData[networkObjectId] = CaptureOriginalData(interactor);
            }
    
            // Start the size change animation
            var coroutine = StartCoroutine(AnimateSizeChange(interactor, networkObjectId, newTargetScale, isReset));
            k_ActiveCoroutines[networkObjectId] = coroutine;
    
            // Play effects
            PlayEffects(interactor.transform.position);
        }
    
        /// <summary>
        /// Coroutine that smoothly animates the scale change on all clients.
        /// </summary>
        private IEnumerator AnimateSizeChange(GameObject interactor, ulong networkObjectId, Vector3 endScale, bool isReset)
        {
            if (interactor == null) yield break;
    
            Vector3 startScale = interactor.transform.localScale;
    
            // Animate the scale change
            float elapsed = 0f;
            while (elapsed < transitionDuration)
            {
                if (interactor == null) yield break;
    
                elapsed += Time.deltaTime;
                float t = scaleCurve.Evaluate(elapsed / transitionDuration);
                interactor.transform.localScale = Vector3.Lerp(startScale, endScale, t);
                yield return null;
            }
    
            // Ensure we end at exactly the target scale
            if (interactor != null)
            {
                interactor.transform.localScale = endScale;
                if (isReset)
                {
                    k_OriginalData.Remove(networkObjectId);
                }
            }
    
            // Clean up coroutine reference
            k_ActiveCoroutines.Remove(networkObjectId);
        }
    
        /// <summary>
        /// Plays visual and audio effects at the specified position.
        /// </summary>
        private void PlayEffects(Vector3 position)
        {
            if (sizeChangeVFX != null)
            {
                CoreDirector.CreatePrefabEffect(sizeChangeVFX)
                    .WithPosition(position)
                    .WithName("SizeChange")
                    .WithDuration(2f)
                    .Create();
            }
    
            if (sizeChangeSound != null)
            {
                CoreDirector.RequestAudio(sizeChangeSound)
                    .WithPosition(position)
                    .Play();
            }
        }
    
        /// <summary>
        /// Captures the original transform and movement data from the interactor.
        /// </summary>
        private TransformData CaptureOriginalData(GameObject interactor)
        {
            var data = new TransformData { originalScale = interactor.transform.localScale, };
            return data;
        }
    
        public void CancelEffect(GameObject interactor)
        {
            if (!interactor.TryGetComponent<NetworkObject>(out var netObj))
            {
                return;
            }
    
            ulong networkId = netObj.NetworkObjectId;
            if (k_ActiveCoroutines.TryGetValue(networkId, out var coroutine))
            {
                if (coroutine != null)
                {
                    StopCoroutine(coroutine);
                }
    
                k_ActiveCoroutines.Remove(networkId);
            }
    
            if (k_OriginalData.TryGetValue(networkId, out var data))
            {
                TriggerSizeChangeRpc(networkId, data.originalScale, true);
            }
        }
    
        /// <summary>
        /// Cleanup when the effect is destroyed or the scene changes.
        /// </summary>
        public override void OnDestroy()
        {
            foreach (var coroutine in k_ActiveCoroutines.Values)
            {
                if (coroutine != null)
                {
                    StopCoroutine(coroutine);
                }
            }
    
            k_ActiveCoroutines.Clear();
        }
    }
    
    

    Usage

    Add the Change Size Effect to any Modular Interactable object.

    Note

    Learn how to create a Modular Interactable object: Creating a Jump Pad.

    In This Article
    Back to top
    Copyright © 2026 Unity Technologies — Trademarks and terms of use
    • Legal
    • Privacy Policy
    • Cookie Policy
    • Do Not Sell or Share My Personal Information
    • Your Privacy Choices (Cookie Settings)