ID:151692
 
Trying to come up with better ways to incorporate sound environments, I was thinking it might work to create static sound objects that are placed on the map, and when told to they will calculate how their sound should be played from each tile.

The sound object, on demand, will loop through all turfs in range. For each tile, it will check to see whether any tiles between it and the sound object are obstructed. If the line between the two points is obstructed, then that tile is added to a list as obstructed. If not, then that tile is added to the list as clear.

Once the object has added all tiles within range to a list, it will loop through each tile and perform another check to determine whether each tile will play the object's sound as direct, obstruction, exclusion or occlusion:



If the tile is obstructed:
If other tiles nearby are obstructed:
Then this tile uses Occlusion.
If other tiles nearby are clear:
Then this tile uses Obstruction.
If the tile is clear:
If other tiles nearby are obstructed:
Then this tile ues Exclusion.
If other tiles nearby are clear:
Then this tile uses Direct.

Then when the player is within range of the object, the object sends the sound to the player, and the sound constantly updates itself to reflect the way it should be played from the tile its on, until the player is no longer within range of the sound object.

That way you could have things on the map such as waterfalls, which play a constant repeating sound, but that sound will be influenced by where you stand in relation to the waterfall.

My only question now is what's a good way to determine which "other tiles nearby" to consider.
Sound travels. Maybe a similar approach to path finding can help. If you compare the real distance and the calculated travel distance, I think you can classify the sound as direct, obstruction, exclusion or occlusion. For example, if the real distance = travel distance, the sound is surely direct.
In response to Jemai1
Jemai1 wrote:
Sound travels. Maybe a similar approach to path finding can help. If you compare the real distance and the calculated travel distance, I think you can classify the sound as direct, obstruction, exclusion or occlusion. For example, if the real distance = travel distance, the sound is surely direct.

Not true. If the real distance = travel distance it could also be exclusion. It takes more than path finding. Plus I'd like to avoid path finding if at all possible because having to find the paths to perhaps 400 tiles for every sound object at the beginning of the game could create a notable delay.
Rather than making some explicit loop for this, you could apply some version of the Observer pattern where, when an object capable of hearing the sound (that is, an object in range) moves, it calculates the sound variation that applies to it on-demand (rather than keeping a resource-hungry loop).

Incidentally, I imagine my Signals&Slots library---hub://Kuraudo.Signals---would be a good method of doing this. Here's one idea:

Your static sound object would have an update_play_variation(turf/T) and a remove_play_variation(turf/T) slot. These might do the following:

update_play_variation(turf/T)
- calculate play variation for T, store it in T
- connect sound object's "play" signal to T's listen() slot

remove_play_variation(turf/T)
- disconnect sound object's "play" signal from T's listen() slot

When created, your sound object goes through all of the turfs in range and connects some "entered" and "exited" signals that those turfs emit in Entered() and Exited(), respectively, to the update_play_variation() and remove_play_variation()...respectively.

So here's what happens. When a player enters a turf in range, that turf emits an "entered" signal. The sound object catches this in its update_play_variation() slot, updating the variation to play to that location (based on obstructions and whatever), and connects its "play" signal to that location, so the turf will hear whatever you play. Then, when the player exits, it simply disconnects the "play" signal, so that the turf doesn't needlessly receive sound.

I went ahead and started writing an example implementation for your convenience. It's far from a finished, working example, but I think it should be better explaining what I'm talking about than I am:
turf
var
signal_mgr/signals = new

const // SIGNALS:
ENTERED = 0
EXITED = 1

/*
The turf stores play variations in an associated list:
sound_obj = variation

This way, the sound variation to use can be stored
for multiple /static_sound objects that this turf
is in range of.
*/


list/play_variations

const // PLAY VARIATIONS:
DIRECT = 0
OBSTRUCTION = 1
EXCLUSION = 2
OCCLUSION = 3

Entered(atom/movable/A)
if(ismob(A))
var/mob/M = A
if(M.client) // Only need to worry about playing if a client is here
signals.emit(ENTERED)

Exited(atom/movable/A)
if(ismob(A))
var/mob/M = A
if(M.client)
signals.emit(EXITED)

proc
connect(static_sound/S)
signals.connect(ENTERED, S, /static_sound/proc/update_play_variation)
signals.connect(EXITED, S, /static_sound/proc/remove_play_variation)

disconnect(static_sound/S)
signals.disconnect(ENTERED, S, /static_sound/proc/update_play_variation)
signals.disconnect(EXITED, S, /static_sound/proc/remove_play_variation)

listen() // slot, you handle the semantics

add_play_variation(static_sound/S, variation)
if(play_variations)
play_variations[S] = variation
else
play_variations = list(S = variation)

remove_play_variation(static_sound/S)
if(play_variations)
play_variations -= S
if(!play_variations.len)
play_variations = null


static_sound
parent_type = /obj

var
signal_mgr/signals = new
play_range = 3

const // SIGNALS:
PLAY = 0

Move(newLoc)
var/tempLoc = loc

. = ..()
if(.) // No need to update if movement fails

/*
I'll just loop through all turfs in range of the old position,
disconnect them, loop through all turfs in range of the new
position, connect them. You could optimize this by not affecting
turfs in range of the old position that are also in range of the
new position.
*/

if(tempLoc)
for(var/turf/T in range(play_range, tempLoc))
T.disconnect(src)
for(var/turf/T in range(play_range, loc))
T.connect(src)

proc
update_play_variation(turf/T) // slot
T.add_play_variation(src, calculate_play_variation(T))
signals.connect(PLAY, T, /turf/proc/listen)

remove_play_variation(turf/T) // slot
signals.disconnect(PLAY, T, /turf/proc/listen)
T.remove_play_variation(src)

calculate_play_variation(turf/T) // this is your job

Edit: It originally came across to me as though you wanted the obstruction calculations to happen regularly; in this case it would happen every time someone enters a new turf in range (and for that specific turf).

However, after a recent post-reply you made, it seems like you only want to calculate for each turf once. That doesn't rule this method out! It still has the benefit of making on-demand calculations. One optimization you can make is to not remove the play variation from the turf when players exit. Leave it saved by the turf. Then, in calculate_play_variation(), you could check if there is already a variation calculated and return that value, else calculate a new one. This is effectively a cache.
What do you suppose would be a good way to handle objects that make sounds when there's a lot of the same object, and you want to hear sound next to any of it. For example, being near a river object makes a river sound, but the river continues for some distance. However, regardless of where you're standing, if you're near the river, you'll hear the river sound. But the further you get from the river as a whole, you fainter the sound will be. Any ideas how to handle that?
In response to Foomer
Foomer wrote:
What do you suppose would be a good way to handle objects that make sounds when there's a lot of the same object, and you want to hear sound next to any of it. For example, being near a river object makes a river sound, but the river continues for some distance. However, regardless of where you're standing, if you're near the river, you'll hear the river sound. But the further you get from the river as a whole, you fainter the sound will be. Any ideas how to handle that?

Dijkstra's algorithm lets you find out what the nearest of any given type is. You could use that to find out how far away the closest river object is.

The problem with this however is it will not generate a difference between someone standing in the middle of a U shaped bend versus the side of a normal river.

If your water isn't likely to be changing locations, you could do this: (this may get a bit intensive though, but it is the most accurate way I can think of)
Give turf's a variable that stores the strength of their river sounds, if any.
Sigh. Time for loops inside of loops...
Loop through every single river object in the world.
For each river object, loop through all of the turfs at the farthest distance away that the river can be heard from. For each step closer to the river the turf is, add one more point to the river sound value.
In response to Foomer
You could use an /area since you won't be constantly exiting and re-entering it as long as you don't actually leave the area around the river. This way the sound would remain constant until you moved away from the area, as opposed to each river turf playing a sound.
In response to Foomer
Could use area to set rooms. If pathfinding has to walk through the area, means it enters different room