I want to create a sound simulation system that emulates what a Geiger counter does. When the player character gets close to a certain region, clicking sounds will start to play. As they get closer, the amount of clicks will increase in a given time-frame.
From what I know, this would be unrealistic/poorly implemented if all I do is play a sound file. What else would I need to couple the audio system with in order to generate these clicks?
One idea I had was to generate random rays from the user and play a clicking sound every time there is a hit with a target, but I worry that the number of rays would need to be excessive in such a scenario.
A simpler version of this would be to create a distance dependent function with a random number generator. As the distance grows smaller, the chance of rng_num > bounding_func(distance) being true would increase and I can play the sound. Here, bounding_func(distance) would convert the linear integer “distance” into something like a logarithmic function, which simulates ray interactions better.
Would these two methods be viable, or is there a better method of implementation for this system?
Distance converts to the chance of an ionizing radiation event as inverse square, so it goes as 1 / (d ^ 2)
If you have many sources then they all sum up to a given rate of ionizing radiation event.
As for implementation, I would just synthesize the clicks yourself and play them out with Unity’s OnAudioFilterRead() mechanism.
In fact, I just did. Full package included, and below is the source.
Enjoy! 
using System.Collections;
using System.Collections.Generic;
using UnityEngine.UI;
// @kurtdekker - Geiger Counter simulator
//
// To use:
// - make a new scene with AudioListener (the one on the Camera will suffice)
// - drop this script on a blank GameObject
// - make a UI.Slider
// - if you don't then you will need to call DriveChance yourself
public class GeigerCounter : MonoBehaviour
{
public class Tick
{
public float amplitude;
public float phase;
}
List<Tick> ticks = new List<Tick>();
[Header("Put a slider in here to feed the chance...")]
public Slider slider;
[Header("Volume")]
public float gain = 0.5F;
// radiation... it's all around you every day
float backgroundRadiation = 0.000001f;
// and this is when you stick your head in the nuclear furnace
float span = 0.0100f;
private bool running = false;
// input
float chance;
int randomRover;
const int numStoredRandoms = 256000;
float[] storedRandoms;
// expects 0.0 to 1.0
public void SetRadiationLevel( float input)
{
// arbitrary power curve - use an AnimationCurve if you want to define the relationship
input = input * input * input;
int sampleRate = AudioSettings.outputSampleRate;
input *= 44100f / sampleRate;
chance = backgroundRadiation + span * input;
}
IEnumerator Start()
{
chance = backgroundRadiation;
storedRandoms = new float[numStoredRandoms];
for (int i = 0; i < numStoredRandoms; i++)
{
storedRandoms[i] = Random.value;
}
gameObject.AddComponent<AudioSource>();
if (slider)
{
slider.onValueChanged.AddListener( delegate(float v) {
SetRadiationLevel(v);
});
}
running = true;
// refresh the random source to keep it new and exciting for the ear
{
int fillRover = 0;
while(true)
{
for(int i = 0; i < numStoredRandoms / 50; i++)
{
storedRandoms[fillRover] = Random.value;
fillRover++;
if (fillRover >= storedRandoms.Length)
{
fillRover = 0;
}
}
yield return null;
}
}
}
void OnAudioFilterRead(float[] data, int channels)
{
if (!running)
return;
int dataLen = data.Length / channels;
int n = 0;
while (n < dataLen)
{
// render all the ongoing ticks
foreach( var tick in ticks)
{
float x = gain * tick.amplitude * Mathf.Sin(tick.phase);
int i = 0;
// random extra idea: might be kinda fun to have each
// tick also track its left / right stereo position,
// so that the ticks seem to come from everywhere...
//
// Volume wise:
// To do this you'd need to choose and store $channels
// worth of overall gain levels per tick and use them
// here when writing the samples into a stream.
//
// Phase error wise:
// I think if each Tick stored a phase error and used
// it to render a second x sample above for left/right
while (i < channels)
{
data[n * channels + i] += x;
i++;
}
tick.phase += tick.amplitude * 0.3f;
tick.amplitude *= 0.993f;
}
// did an ionizing radiation event occur this sample frame?
float v = storedRandoms[randomRover];
if (v < chance)
{
var tick = new Tick()
{
amplitude = 1.0f,
};
ticks.Add(tick);
}
randomRover++;
if (randomRover >= storedRandoms.Length)
{
randomRover = 0;
}
n++;
}
// tidy up lazily when ticks get pretty quiet
ticks.RemoveAll( x => x.amplitude < 0.01f);
}
}
GeigerCounter.unitypackage (6.7 KB)
2 Likes
Thank you very much! This is exactly what I was imagining/aiming for. I just wanted to add onto this a few things I changed.
Since the radiation event is distance dependent I removed the slider. The value of SetRadiationLevel is instead calculated with 1 / (dist/10 + 1)^2
where dist is simply Vector3.Distance(cameraPos, this.pos)
.
I experimented with tanh( 1 / (dist)^2 )
as it is more realistic but the clicking chance increases too fast at a certain distance, but I want it to be more gradual. That’s why I opted to just shift the inverse square by one, which ensures the values are bounded by [0,1]. I also divide distance by 10 (or any other int) to spread the function out more. This ensures that the increase in radiation level is much more gradual, allowing for a sort of early warning.
1 Like
Not sure if you noticed my little comment on line 48… Unity has something called an AnimationCurve (worst name ever) that lets you visually draw out a function curve in the editor and then use it in code. It might be useful for your gameplay, to make clear distinct bands of approaching radiation, so as you point out you get some early warning, followed by some “oh man, be careful, it’s close,” followed by a furious bzzzzzt-and-you-are-roasted kinda zone.
All closed up they look like:
But then when you click on them you get a big editor:
You can use the output from the function above to also decide how dead the player is, not just drive the Geiger counter, obviously.
1 Like
Ah yeah thanks for pointing it out. I did see it but I left it for later to investigate. This is the first time I’ve heard about animation curves. I just tested the curve on desmos to get a prototype working :P.
Though now that I see what animation curves are, I can see how convenient they can be. Not sure how much I need it for this specific project but will definitely keep this in mind for the future!
1 Like