Lesson 7: Audio & Visual Polish

Welcome back! Now that your mobile puzzle game has a beautiful UI, it's time to add the finishing touches that make it feel professional and polished. In this lesson, you'll learn how to add mobile-optimized audio, visual effects, and haptic feedback that enhance the player experience without draining battery life.

Painting🖌️🪣🎨 Painting🖌️🪣🎨 by Dribbble Artist

What You'll Learn

  • Add mobile-optimized audio that enhances gameplay
  • Create visual effects that feel satisfying
  • Implement haptic feedback for tactile responses
  • Optimize audio and effects for battery life
  • Balance polish with performance
  • Create memorable audio-visual moments

Prerequisites

  • Unity 2022.3 LTS or newer
  • Completed Lesson 6: Mobile UI/UX Design
  • Basic understanding of Unity Audio and Particle Systems
  • Your mobile puzzle game project from previous lessons

Why Audio & Visual Polish Matters

Great audio and visual polish transform a functional game into an engaging experience:

  • Audio Feedback - Players know their actions worked
  • Visual Effects - Satisfying moments feel rewarding
  • Haptic Feedback - Tactile responses increase immersion
  • Polish - Professional feel builds player trust
  • Retention - Polished games keep players coming back

Without proper polish:

  • Flat Experience - Game feels unfinished
  • Low Engagement - Players lose interest quickly
  • Poor Reviews - Unpolished games get negative feedback
  • Missed Opportunities - Great gameplay wasted on poor presentation

Understanding Mobile Audio Constraints

Mobile devices have unique audio constraints:

  • Battery Life - Audio processing drains battery
  • File Size - Large audio files increase app size
  • Memory - Limited RAM for audio buffers
  • Performance - Too many sounds can cause lag
  • User Preferences - Players may mute audio

Step 1: Set Up Mobile Audio System

Create an efficient audio system for mobile games.

Audio Manager Setup

public class MobileAudioManager : MonoBehaviour
{
    [Header("Audio Sources")]
    public AudioSource musicSource;
    public AudioSource sfxSource;
    public AudioSource uiSource;

    [Header("Audio Settings")]
    public float masterVolume = 1f;
    public float musicVolume = 0.7f;
    public float sfxVolume = 1f;
    public float uiVolume = 0.8f;

    [Header("Mobile Optimization")]
    public int maxSimultaneousSounds = 8;
    public bool enable3DAudio = false; // Disable for 2D games

    private Queue<AudioSource> audioSourcePool = new Queue<AudioSource>();
    private List<AudioSource> activeSources = new List<AudioSource>();

    void Start()
    {
        InitializeAudioSources();
        LoadAudioSettings();
    }

    private void InitializeAudioSources()
    {
        // Create pooled audio sources
        for (int i = 0; i < maxSimultaneousSounds; i++)
        {
            GameObject audioObj = new GameObject($"AudioSource_{i}");
            audioObj.transform.SetParent(transform);
            AudioSource source = audioObj.AddComponent<AudioSource>();
            source.playOnAwake = false;
            audioSourcePool.Enqueue(source);
        }
    }

    public void PlaySFX(AudioClip clip, float volume = 1f)
    {
        if (clip == null) return;

        AudioSource source = GetAvailableSource();
        if (source != null)
        {
            source.clip = clip;
            source.volume = sfxVolume * masterVolume * volume;
            source.Play();
            StartCoroutine(ReturnToPool(source, clip.length));
        }
    }

    private AudioSource GetAvailableSource()
    {
        // Check pool first
        if (audioSourcePool.Count > 0)
        {
            AudioSource source = audioSourcePool.Dequeue();
            activeSources.Add(source);
            return source;
        }

        // Reuse oldest source if pool is empty
        if (activeSources.Count > 0)
        {
            AudioSource oldest = activeSources[0];
            activeSources.RemoveAt(0);
            oldest.Stop();
            return oldest;
        }

        return null;
    }

    private IEnumerator ReturnToPool(AudioSource source, float duration)
    {
        yield return new WaitForSeconds(duration);
        source.Stop();
        activeSources.Remove(source);
        audioSourcePool.Enqueue(source);
    }
}

Audio Settings Management

public class AudioSettings : MonoBehaviour
{
    [Header("Settings Keys")]
    private const string MASTER_VOLUME_KEY = "MasterVolume";
    private const string MUSIC_VOLUME_KEY = "MusicVolume";
    private const string SFX_VOLUME_KEY = "SFXVolume";
    private const string MUTE_KEY = "AudioMuted";

    public void SaveAudioSettings()
    {
        PlayerPrefs.SetFloat(MASTER_VOLUME_KEY, MobileAudioManager.Instance.masterVolume);
        PlayerPrefs.SetFloat(MUSIC_VOLUME_KEY, MobileAudioManager.Instance.musicVolume);
        PlayerPrefs.SetFloat(SFX_VOLUME_KEY, MobileAudioManager.Instance.sfxVolume);
        PlayerPrefs.SetInt(MUTE_KEY, AudioListener.volume > 0 ? 0 : 1);
        PlayerPrefs.Save();
    }

    public void LoadAudioSettings()
    {
        MobileAudioManager.Instance.masterVolume = PlayerPrefs.GetFloat(MASTER_VOLUME_KEY, 1f);
        MobileAudioManager.Instance.musicVolume = PlayerPrefs.GetFloat(MUSIC_VOLUME_KEY, 0.7f);
        MobileAudioManager.Instance.sfxVolume = PlayerPrefs.GetFloat(SFX_VOLUME_KEY, 1f);

        bool muted = PlayerPrefs.GetInt(MUTE_KEY, 0) == 1;
        AudioListener.volume = muted ? 0 : 1;
    }
}

Step 2: Add Sound Effects

Create a comprehensive sound effect system for your puzzle game.

Sound Effect Categories

UI Sounds:

  • Button clicks
  • Menu transitions
  • Notification sounds
  • Achievement unlocks

Gameplay Sounds:

  • Piece placement
  • Match completion
  • Level complete
  • Error/wrong move

Feedback Sounds:

  • Success chimes
  • Failure sounds
  • Progress indicators
  • Power-up activations

Sound Effect Implementation

[System.Serializable]
public class SoundEffect
{
    public string name;
    public AudioClip clip;
    public float volume = 1f;
    public float pitch = 1f;
    public bool loop = false;
}

public class PuzzleSoundEffects : MonoBehaviour
{
    [Header("UI Sounds")]
    public SoundEffect buttonClick;
    public SoundEffect menuOpen;
    public SoundEffect menuClose;

    [Header("Gameplay Sounds")]
    public SoundEffect piecePlace;
    public SoundEffect matchComplete;
    public SoundEffect levelComplete;
    public SoundEffect wrongMove;

    [Header("Feedback Sounds")]
    public SoundEffect success;
    public SoundEffect failure;
    public SoundEffect powerUp;

    private MobileAudioManager audioManager;

    void Start()
    {
        audioManager = FindObjectOfType<MobileAudioManager>();
    }

    public void PlayButtonClick()
    {
        audioManager?.PlaySFX(buttonClick.clip, buttonClick.volume);
    }

    public void PlayMatchComplete()
    {
        audioManager?.PlaySFX(matchComplete.clip, matchComplete.volume);
    }

    public void PlayLevelComplete()
    {
        audioManager?.PlaySFX(levelComplete.clip, levelComplete.volume);
    }
}

Audio Optimization Tips

File Format:

  • Use Vorbis compression for mobile (Unity default)
  • Quality: 70% for music, 50-60% for SFX
  • Load Type: Compressed in Memory for frequently played sounds
  • Load Type: Streaming for background music

File Size:

  • Keep sound effects under 1MB each
  • Use mono audio for positional sounds
  • Compress music files appropriately
  • Remove silence from audio files

Step 3: Add Background Music

Implement dynamic music that enhances the gameplay experience.

Music System

public class MusicManager : MonoBehaviour
{
    [Header("Music Tracks")]
    public AudioClip menuMusic;
    public AudioClip gameplayMusic;
    public AudioClip victoryMusic;

    [Header("Settings")]
    public float fadeDuration = 1f;
    public bool crossfade = true;

    private AudioSource currentSource;
    private AudioSource nextSource;
    private Coroutine fadeCoroutine;

    void Start()
    {
        // Create two audio sources for crossfading
        currentSource = gameObject.AddComponent<AudioSource>();
        nextSource = gameObject.AddComponent<AudioSource>();

        currentSource.loop = true;
        nextSource.loop = true;

        PlayMusic(menuMusic);
    }

    public void PlayMusic(AudioClip clip, bool fade = true)
    {
        if (clip == null) return;

        if (fade && currentSource.isPlaying)
        {
            StartCoroutine(CrossfadeMusic(clip));
        }
        else
        {
            currentSource.clip = clip;
            currentSource.Play();
        }
    }

    private IEnumerator CrossfadeMusic(AudioClip clip)
    {
        nextSource.clip = clip;
        nextSource.volume = 0f;
        nextSource.Play();

        float elapsed = 0f;
        while (elapsed < fadeDuration)
        {
            elapsed += Time.deltaTime;
            float t = elapsed / fadeDuration;

            currentSource.volume = Mathf.Lerp(1f, 0f, t);
            nextSource.volume = Mathf.Lerp(0f, 1f, t);

            yield return null;
        }

        currentSource.Stop();
        AudioSource temp = currentSource;
        currentSource = nextSource;
        nextSource = temp;
    }

    public void SetMusicVolume(float volume)
    {
        currentSource.volume = volume;
        nextSource.volume = volume;
    }
}

Dynamic Music

public class DynamicMusic : MonoBehaviour
{
    [Header("Music Layers")]
    public AudioClip baseLayer;
    public AudioClip intensityLayer;
    public AudioClip victoryLayer;

    [Header("Intensity Settings")]
    public float intensityThreshold = 0.7f;

    private AudioSource[] musicSources;
    private float currentIntensity = 0f;

    void Start()
    {
        musicSources = new AudioSource[3];
        for (int i = 0; i < 3; i++)
        {
            GameObject sourceObj = new GameObject($"MusicLayer_{i}");
            sourceObj.transform.SetParent(transform);
            musicSources[i] = sourceObj.AddComponent<AudioSource>();
            musicSources[i].loop = true;
            musicSources[i].volume = 0f;
        }

        musicSources[0].clip = baseLayer;
        musicSources[1].clip = intensityLayer;
        musicSources[2].clip = victoryLayer;

        musicSources[0].Play();
        musicSources[1].Play();
        musicSources[2].Play();
    }

    public void UpdateIntensity(float intensity)
    {
        currentIntensity = Mathf.Clamp01(intensity);

        // Base layer always plays
        musicSources[0].volume = 1f;

        // Intensity layer fades in based on intensity
        musicSources[1].volume = currentIntensity >= intensityThreshold ? 
            (currentIntensity - intensityThreshold) / (1f - intensityThreshold) : 0f;

        // Victory layer for special moments
        musicSources[2].volume = currentIntensity >= 0.95f ? 1f : 0f;
    }
}

Step 4: Create Visual Effects

Add particle effects and visual polish to your puzzle game.

Particle Effect Manager

public class ParticleEffectManager : MonoBehaviour
{
    [Header("Effect Prefabs")]
    public GameObject matchEffectPrefab;
    public GameObject comboEffectPrefab;
    public GameObject levelCompleteEffectPrefab;
    public GameObject powerUpEffectPrefab;

    [Header("Pool Settings")]
    public int poolSize = 20;

    private Dictionary<GameObject, Queue<GameObject>> effectPools = new Dictionary<GameObject, Queue<GameObject>>();

    void Start()
    {
        InitializePools();
    }

    private void InitializePools()
    {
        CreatePool(matchEffectPrefab);
        CreatePool(comboEffectPrefab);
        CreatePool(levelCompleteEffectPrefab);
        CreatePool(powerUpEffectPrefab);
    }

    private void CreatePool(GameObject prefab)
    {
        if (prefab == null) return;

        Queue<GameObject> pool = new Queue<GameObject>();
        for (int i = 0; i < poolSize; i++)
        {
            GameObject obj = Instantiate(prefab);
            obj.SetActive(false);
            obj.transform.SetParent(transform);
            pool.Enqueue(obj);
        }

        effectPools[prefab] = pool;
    }

    public void PlayEffect(GameObject prefab, Vector3 position)
    {
        if (prefab == null || !effectPools.ContainsKey(prefab)) return;

        Queue<GameObject> pool = effectPools[prefab];

        GameObject effect = null;
        if (pool.Count > 0)
        {
            effect = pool.Dequeue();
        }
        else
        {
            effect = Instantiate(prefab);
            effect.transform.SetParent(transform);
        }

        effect.transform.position = position;
        effect.SetActive(true);

        ParticleSystem particles = effect.GetComponent<ParticleSystem>();
        if (particles != null)
        {
            particles.Play();
            StartCoroutine(ReturnToPool(effect, prefab, particles.main.duration + particles.main.startLifetime.constantMax));
        }
    }

    private IEnumerator ReturnToPool(GameObject effect, GameObject prefab, float duration)
    {
        yield return new WaitForSeconds(duration);
        effect.SetActive(false);
        effectPools[prefab].Enqueue(effect);
    }
}

Match Effect System

public class MatchEffect : MonoBehaviour
{
    [Header("Effect Components")]
    public ParticleSystem particles;
    public Animator animator;
    public AudioSource audioSource;

    [Header("Effect Settings")]
    public Color matchColor = Color.white;
    public float effectDuration = 1f;

    public void PlayMatchEffect(Vector3 position, int matchCount)
    {
        transform.position = position;

        // Adjust particle color based on match count
        var main = particles.main;
        main.startColor = matchColor;

        // Scale effect based on match size
        float scale = 1f + (matchCount - 3) * 0.2f;
        transform.localScale = Vector3.one * scale;

        // Play particles
        particles.Play();

        // Play animation
        if (animator != null)
        {
            animator.SetTrigger("Play");
        }

        // Play sound
        if (audioSource != null)
        {
            audioSource.pitch = 1f + (matchCount - 3) * 0.1f;
            audioSource.Play();
        }

        // Auto-destroy after duration
        Destroy(gameObject, effectDuration);
    }
}

Step 5: Implement Haptic Feedback

Add tactile feedback that enhances the mobile experience.

Haptic Feedback System

public class HapticFeedback : MonoBehaviour
{
    public enum HapticType
    {
        Light,
        Medium,
        Heavy,
        Success,
        Failure,
        Selection
    }

    public void TriggerHaptic(HapticType type)
    {
        #if UNITY_IOS
        TriggerIOSHaptic(type);
        #elif UNITY_ANDROID
        TriggerAndroidHaptic(type);
        #else
        // Fallback for editor/testing
        Handheld.Vibrate();
        #endif
    }

    #if UNITY_IOS
    private void TriggerIOSHaptic(HapticType type)
    {
        switch (type)
        {
            case HapticType.Light:
                // UIImpactFeedbackGenerator (light)
                break;
            case HapticType.Medium:
                // UIImpactFeedbackGenerator (medium)
                break;
            case HapticType.Heavy:
                // UIImpactFeedbackGenerator (heavy)
                break;
            case HapticType.Success:
                // UINotificationFeedbackGenerator (success)
                break;
            case HapticType.Failure:
                // UINotificationFeedbackGenerator (error)
                break;
            case HapticType.Selection:
                // UISelectionFeedbackGenerator
                break;
        }
    }
    #endif

    #if UNITY_ANDROID
    private void TriggerAndroidHaptic(HapticType type)
    {
        long[] pattern;
        int[] amplitudes;

        switch (type)
        {
            case HapticType.Light:
                pattern = new long[] { 0, 10 };
                amplitudes = new int[] { 0, 50 };
                break;
            case HapticType.Medium:
                pattern = new long[] { 0, 20 };
                amplitudes = new int[] { 0, 100 };
                break;
            case HapticType.Heavy:
                pattern = new long[] { 0, 30 };
                amplitudes = new int[] { 0, 255 };
                break;
            case HapticType.Success:
                pattern = new long[] { 0, 50, 50, 50 };
                amplitudes = new int[] { 0, 100, 0, 150 };
                break;
            case HapticType.Failure:
                pattern = new long[] { 0, 100, 50, 100 };
                amplitudes = new int[] { 0, 255, 0, 200 };
                break;
            case HapticType.Selection:
                pattern = new long[] { 0, 15 };
                amplitudes = new int[] { 0, 75 };
                break;
            default:
                Handheld.Vibrate();
                return;
        }

        // Use Android's VibrationEffect API
        AndroidJavaClass unityPlayer = new AndroidJavaClass("com.unity3d.player.UnityPlayer");
        AndroidJavaObject currentActivity = unityPlayer.GetStatic<AndroidJavaObject>("currentActivity");
        AndroidJavaObject vibrator = currentActivity.Call<AndroidJavaObject>("getSystemService", "vibrator");

        if (vibrator != null)
        {
            vibrator.Call("vibrate", pattern, -1);
        }
    }
    #endif
}

Haptic Integration

public class PuzzleHaptics : MonoBehaviour
{
    private HapticFeedback haptic;

    void Start()
    {
        haptic = FindObjectOfType<HapticFeedback>();
    }

    public void OnPiecePlaced()
    {
        haptic?.TriggerHaptic(HapticFeedback.HapticType.Light);
    }

    public void OnMatchComplete(int matchCount)
    {
        if (matchCount >= 5)
        {
            haptic?.TriggerHaptic(HapticFeedback.HapticType.Heavy);
        }
        else
        {
            haptic?.TriggerHaptic(HapticFeedback.HapticType.Medium);
        }
    }

    public void OnLevelComplete()
    {
        haptic?.TriggerHaptic(HapticFeedback.HapticType.Success);
    }

    public void OnWrongMove()
    {
        haptic?.TriggerHaptic(HapticFeedback.HapticType.Failure);
    }

    public void OnButtonPress()
    {
        haptic?.TriggerHaptic(HapticFeedback.HapticType.Selection);
    }
}

Step 6: Optimize for Battery Life

Ensure your audio and effects don't drain battery unnecessarily.

Battery Optimization

public class BatteryOptimizer : MonoBehaviour
{
    [Header("Optimization Settings")]
    public bool reduceParticlesOnLowBattery = true;
    public bool reduceAudioOnLowBattery = true;
    public float lowBatteryThreshold = 0.2f;

    [Header("Quality Levels")]
    public int particleQuality = 2; // 0=Low, 1=Medium, 2=High
    public int audioQuality = 2;

    private float lastBatteryCheck = 0f;
    private float batteryCheckInterval = 5f;

    void Update()
    {
        if (Time.time - lastBatteryCheck > batteryCheckInterval)
        {
            CheckBatteryLevel();
            lastBatteryCheck = Time.time;
        }
    }

    private void CheckBatteryLevel()
    {
        float batteryLevel = SystemInfo.batteryLevel;

        if (batteryLevel < lowBatteryThreshold)
        {
            if (reduceParticlesOnLowBattery)
            {
                ReduceParticleQuality();
            }

            if (reduceAudioOnLowBattery)
            {
                ReduceAudioQuality();
            }
        }
    }

    private void ReduceParticleQuality()
    {
        ParticleSystem[] particles = FindObjectsOfType<ParticleSystem>();
        foreach (ParticleSystem ps in particles)
        {
            var main = ps.main;
            main.maxParticles = Mathf.Max(10, main.maxParticles / 2);
        }
    }

    private void ReduceAudioQuality()
    {
        AudioSource[] audioSources = FindObjectsOfType<AudioSource>();
        foreach (AudioSource source in audioSources)
        {
            if (source.clip != null && source.clip.loadType == AudioClipLoadType.DecompressOnLoad)
            {
                source.volume *= 0.7f;
            }
        }
    }
}

Performance Monitoring

public class AudioPerformanceMonitor : MonoBehaviour
{
    [Header("Monitoring")]
    public bool logPerformance = false;
    public float checkInterval = 1f;

    private int activeAudioSources = 0;
    private float audioMemoryUsage = 0f;

    void Start()
    {
        InvokeRepeating(nameof(CheckAudioPerformance), 0f, checkInterval);
    }

    private void CheckAudioPerformance()
    {
        AudioSource[] sources = FindObjectsOfType<AudioSource>();
        activeAudioSources = 0;

        foreach (AudioSource source in sources)
        {
            if (source.isPlaying)
            {
                activeAudioSources++;
            }
        }

        if (logPerformance)
        {
            Debug.Log($"Active Audio Sources: {activeAudioSources}");
        }

        // Warn if too many sources active
        if (activeAudioSources > 10)
        {
            Debug.LogWarning("Too many audio sources active! Consider pooling.");
        }
    }
}

Step 7: Create Visual Feedback Systems

Add visual feedback that responds to player actions.

Screen Effects

public class ScreenEffects : MonoBehaviour
{
    [Header("Effect Components")]
    public Camera mainCamera;
    public Image flashOverlay;

    [Header("Effect Settings")]
    public float flashDuration = 0.2f;
    public Color flashColor = Color.white;

    public void FlashScreen(Color color, float duration)
    {
        StartCoroutine(FlashCoroutine(color, duration));
    }

    private IEnumerator FlashCoroutine(Color color, float duration)
    {
        flashOverlay.color = color;
        flashOverlay.gameObject.SetActive(true);

        float elapsed = 0f;
        while (elapsed < duration)
        {
            elapsed += Time.deltaTime;
            float alpha = Mathf.Lerp(1f, 0f, elapsed / duration);
            flashOverlay.color = new Color(color.r, color.g, color.b, alpha);
            yield return null;
        }

        flashOverlay.gameObject.SetActive(false);
    }

    public void ShakeCamera(float intensity, float duration)
    {
        StartCoroutine(ShakeCoroutine(intensity, duration));
    }

    private IEnumerator ShakeCoroutine(float intensity, float duration)
    {
        Vector3 originalPosition = mainCamera.transform.localPosition;
        float elapsed = 0f;

        while (elapsed < duration)
        {
            elapsed += Time.deltaTime;
            float currentIntensity = intensity * (1f - elapsed / duration);

            Vector3 shakeOffset = new Vector3(
                Random.Range(-1f, 1f),
                Random.Range(-1f, 1f),
                0f
            ) * currentIntensity;

            mainCamera.transform.localPosition = originalPosition + shakeOffset;
            yield return null;
        }

        mainCamera.transform.localPosition = originalPosition;
    }
}

UI Animation System

public class UIAnimations : MonoBehaviour
{
    [Header("Animation Settings")]
    public float bounceScale = 1.2f;
    public float bounceDuration = 0.3f;

    public void BounceButton(RectTransform button)
    {
        StartCoroutine(BounceCoroutine(button));
    }

    private IEnumerator BounceCoroutine(RectTransform button)
    {
        Vector3 originalScale = button.localScale;
        float elapsed = 0f;

        while (elapsed < bounceDuration)
        {
            elapsed += Time.deltaTime;
            float t = elapsed / bounceDuration;

            // Bounce animation curve
            float scale = 1f + (bounceScale - 1f) * Mathf.Sin(t * Mathf.PI);
            button.localScale = originalScale * scale;

            yield return null;
        }

        button.localScale = originalScale;
    }

    public void PulseElement(RectTransform element, float pulseAmount = 0.1f)
    {
        StartCoroutine(PulseCoroutine(element, pulseAmount));
    }

    private IEnumerator PulseCoroutine(RectTransform element, float pulseAmount)
    {
        Vector3 originalScale = element.localScale;
        float elapsed = 0f;
        float duration = 1f;

        while (true)
        {
            elapsed += Time.deltaTime;
            float t = Mathf.Sin(elapsed * 2f * Mathf.PI / duration);
            float scale = 1f + pulseAmount * t;
            element.localScale = originalScale * scale;
            yield return null;
        }
    }
}

Mini Challenge: Add Polish to Your Game

Add comprehensive audio and visual polish to your puzzle game:

  1. Audio System - Set up audio manager with sound effects
  2. Background Music - Add dynamic music that responds to gameplay
  3. Visual Effects - Create particle effects for matches and combos
  4. Haptic Feedback - Implement tactile responses for actions
  5. Screen Effects - Add camera shake and flash effects
  6. UI Animations - Animate buttons and UI elements
  7. Optimization - Ensure everything runs smoothly on mobile

Success Criteria:

  • All actions have audio feedback
  • Visual effects enhance gameplay feel
  • Haptic feedback works on supported devices
  • Performance remains smooth (60 FPS)
  • Battery usage is reasonable

Common Mistakes to Avoid

1. Too Many Sounds Playing

  • Limit simultaneous audio sources
  • Use audio pooling
  • Prioritize important sounds

2. Large Audio Files

  • Compress audio appropriately
  • Use streaming for music
  • Keep SFX files small

3. Overusing Effects

  • Don't add effects to every action
  • Use effects for important moments
  • Balance visual noise

4. Ignoring Battery Life

  • Monitor audio/effect performance
  • Reduce quality on low battery
  • Allow players to disable effects

5. No User Controls

  • Provide audio volume sliders
  • Allow muting music/SFX separately
  • Save user preferences

Pro Tips

Audio Design

  • Use audio to guide player attention
  • Create audio feedback loops
  • Test audio on actual devices
  • Consider players who play muted

Visual Effects

  • Less is often more
  • Use effects to highlight important moments
  • Create reusable effect prefabs
  • Optimize particle counts

Haptic Feedback

  • Use haptics sparingly
  • Match haptic intensity to action importance
  • Test on different devices
  • Provide option to disable

Performance

  • Profile audio and effects regularly
  • Use object pooling for effects
  • Compress audio files appropriately
  • Monitor battery impact

Troubleshooting

Audio not playing

  • Check audio source settings
  • Verify audio clips are assigned
  • Check volume settings
  • Ensure audio isn't muted

Effects causing lag

  • Reduce particle counts
  • Use simpler effects
  • Disable effects on low-end devices
  • Optimize particle systems

Battery draining quickly

  • Reduce audio quality
  • Lower particle counts
  • Disable unnecessary effects
  • Monitor performance

Haptics not working

  • Check device support
  • Verify platform-specific code
  • Test on actual devices
  • Check permissions

What's Next?

Congratulations! You've added professional audio and visual polish to your mobile puzzle game. In the next lesson, you'll learn how to optimize your game for performance and battery life, ensuring it runs smoothly on all mobile devices.

Coming Up: Lesson 8 will cover performance optimization, battery efficiency, memory management, and ensuring your game runs smoothly on low-end devices.

Resources

Community Support

  • Discord Server: Share your polished game and get feedback
  • GitHub Repository: Find audio and effect examples
  • Game Development Forums: Learn from other mobile developers
  • Course Discussion: Show off your audio-visual polish

Ready to optimize your game for performance? Continue to Lesson 8: Performance & Battery Optimization and learn how to make your game run smoothly on all devices.