How can I locally play audio over a network without rapid RPC calls?

I’m currently working on a FPS game. I had audio working quite well on single player for my rifle (fires about every .1 second), but then I started using RPC calls for other players to hear the audio as well. This caused a terrible stutter in the audio, and reduced the quality greatly. Is there a way I could have the players locally determine the shots and play them at the shooting player’s point? I want to try to avoid calling excessive amounts of RPCs, to avoid the bandwidth and lag issues.

My code Currently:

GetComponent().RPC(“Fire”, PhotonTargets.Others, gunAudioClip, transform.position); //(called every .1 seconds)

[RPC]
public void Fire(int gunAudio, Vector3 position)
{

    AudioSource.PlayClipAtPoint(AudioClips[gunAudio], position);
}

If you want players to receive “input” information (when they are firing) from other players, you won’t have choice but to inform them, which mean RPC in this case.

First, since you are calling the same Audioclip on the same AudioSource to play the sound so often, it doesn’t have time to finish playing it, that you’re calling again to play it, so it just keep restarting it without fully playing it (except the last time it get called of course). You need to have one AudioSource for every sound you wish to play simultaneously.

But creating tons of AudioSource isn’t really performance and memory efficient, also you would need to destroy them once you dont need them anymore, so I strongly advise you to use a pooling system for this. There is great plugins in the Unity Asset Store that can do it for you, or if you wish to implement your own pooling here is an exemple:

public class AudioPoolManager : MonoBehaviour {

    // The amount of AudioSource we will initialize the pool with
    public int poolSize = 10;

    // We use a queue since it will remove the instance at the same time that we are asking for one
    private Queue<AudioSource> pool;

    void Awake()
    {
        pool = new List<AudioSource>();

        // Here we create initialize our pool with the specified amount of instance,
        for (int i = 0; i < poolSize; i++) 
        {
            pool.Add(CreateNewInstance());
        }
    }

    private AudioSource CreateNewInstance() 
    {
        GameObject go = new GameObject("AudioSourceInstance");
        // Let's group in our instance under the pool manager
        go.transform.parent = this.transform;
        AudioSource as = go.AddComponent<AudioSource>();

        return as;
    }

    // When we are asking for an AudioSource, we will first check if we still have one in our
    // pool, if not create a new instance
    public AudioSource GetAudioSource()
    {
        if (pool.Count < 1) {
            return CreateNewInstance();
        } else {
            return pool.Dequeue();
        }
    }

    // Always return the AudioSource instance once you are done with it
    public void ReturnAudioSource(AudioSource instance) 
    {
        pool.Add(instance);
    }
}

So here is how you would use it:

// Keep a reference somewhere
public AudioPoolManager audioPool;

[RPC]
public void Fire(Vector3 position)
{
    AudioSource as = audioPool.GetAudioSource();
    as.PlayClipAtPoint(gunAudio, position);

    // We need to know when the audio clip has finished playing
    // So we can return the audio source instance to the pool manager
    StartCoroutine(WaitForAudioClipToFinish(as, gunAudio.length));
}

private Enumerator WaitForAudioClipToFinish(AudioSource as, float audioClipDuration)
{
    yield return new WaitForSeconds(audioClipDuration);
    audioPool.ReturnAudioSource(as);
} 

Also, you shouldn’t send the Audioclip with your RPC. If you have only one sounds that can be played, there is no need to send it, just keep a reference on the other player object and play that sound with that reference. If you wish to play multiple sound use an byte enum to identify which sound to play.

public Audioclip rifleAudio;
public Audioclip shotgunAudio;

public enum PlayerSound : byte
{
    rifleSound,
    shotgunSound
    // etc...
}

public void Fire()
{
    GetComponent().RPC("Fire", PhotonTargets.Others, (byte)PlayerSound.rifleSound, transform.position);
}

[RPC]
public void Fire(byte playerSoundByte, Vector3 position)
{
    PlayerSound soundType = (PlayerSound)playerSoundByte;

    if (soundType == PlayerSound.rifleSound) {
        AudioSource.PlayClipAtPoint(rifleAudio, position);
    } else if (soundType == PlayerSound.shotgunSound) {
        AudioSource.PlayClipAtPoint(shotgunAudio, position);
    }
}

Assuming you’re creating an authoritative server, you should just have the server instantiate the object with an audio clip. So just add if(Network.isServer) whenever you call your RPC. Alternatively, you can make a script that just holds all possible audio clips and attach it to the players individually. Then, when you want that sound to play, just call an RPC function with a for each loop.

Something like:

GameObject[] players = GameObject.FindGameObjectsWithTag("Player");
foreach (GameObject player in players){
AudioScript audioScript = player.GetComponent<AudioScript>();
audioScript.PlaySound(audioScript.sound1);
}

Then Audio Script would look like so:

public AudioSource source;
public AudioClip sound1;

public void PlaySound(Audioclip sound){
source.PlayOneShot(sound);
}

}

Good luck!