Monsters that can be alerted by sounds

Hi. I am trying to create a monster that goes off sounds, a reference point would be the Warden from Minecraft and how he only goes after players by sound how would I achieve this?

3 Likes

Create a brick whenever a sound is played and use PathfindingService to go toward that brick. Simple.

2 Likes

but how do you detect that? if you know please tell me

trigger a remoteEvent with a script that detects when a sound is made. That event will create a part where the sound was played and the monster will make a beeline to that part using pathFindingService.

I’m good with the pathfinding services thing but how would the remote event work like that??? can you show me an example script?

Unfortunately, I’m not super experienced with exactly how to make that script. You should look at the articles on remote events and pathfinding service. Plus, it’d be much better if you learned how to make it work.

1 Like

The hard part is actually detecting when a sound is played.
The two ways of doing that are:

  1. Find each and every sound, and connect to its .Played event. Any time this event fires, alert nearby monsters.
  2. Make a custom function to play a sound. This function will both play the sound, and alert nearby monsters.

The first option is inefficient and requires care to get any and all sounds and avoid memory leaks, but is self-contained.

for _,sound in ipairs(workspace:GetDescendants) do -- look at EVERY SINGLE THING in workspace and check for sounds
	if sound:IsA("Sound") then
		sound.Played:Connect(function() SoundHasPlayed(sound) end)
	end
end

-- for good measure, make every new sound do that
workspace.DescendantAdded:Connect(function(sound)
	if sound:IsA("Sound") then
		sound.Played:Connect(function() SoundHasPlayed(sound) end)
	end
end

function SoundHasPlayed(sound)
	-- where is the sound, anyway?
	local parent = sound.Parent
	if parent == nil then return end -- do nothing if the sound shouldn't exist at all
	
	if parent:IsA("BasePart") then
		SoundPlayedSomewhere(sound, parent.Position)
	elseif parent:IsA("Attachment") then
		SoundPlayedSomewhere(sound, parent.WorldPosition)
	else
		-- environmental sound such as music which has no actual "position".
		-- alert nobody
	end
end

The second option is much more efficient, but requires changing every script that plays sounds to make them use the new function.

function PlaySoundAndAlertMonsters(sound)
	sound:Play()
	SoundHasPlayed(sound)
end

“Alert nearby monsters” could either be some code that loops through every monster and checks if they heard it and makes them move toward the sound.

function SoundPlayedSomewhere(sound, location)
	for _,monster in pairs(ListOfAllMonsters) do -- this list is hard to get
		MonsterHearsSound(monster, sound, location)
	end
end

function MonsterHearsSound(monster, sound, location)
	if monster.Name == "Zombie" then
		-- run toward the sound
		-- etc.
	elseif monster.Name == "Chicken" then
		-- run away if it's close
	end
end

But it could instead fire a BindableEvent, the code in the monster could subscribe to this event (which is now well and truly a “sound played somewhere” event!).

local event = game.ServerStorage.SoundPlayedEvent
function SoundPlayedSomewhere(sound, location)
	event:Fire(sound, location)
end

-- code in a Zombie
local me = script.Parent.PrimaryPart -- any part that's attached to the zombie. is usually the HumanoidRootPart
local humanoid = script.Parent.Humanoid
function onHearSound(sound, location)
	-- run toward the sound
	if me.Position:FuzzyEq(location, 20) then -- was the sound heard? (is it less than 20 studs away?)
		humanoid:MoveTo(location) -- move to the location of the sound
	end
end
game.ServerStorage.SoundPlayedEvent.Event:Connect(onHearSound)
-- Note that this will be a very dumb Zombie who will instantly run to every sound as soon as it plays, even if it's behind him

-- code in a Chicken
local me = script.Parent.PrimaryPart
local humanoid = script.Parent.Humanoid
function onHearSound(sound, location)
	-- run away if it's close (8 studs, for example)
	if me.Position:FuzzyEq(location, 8) then
		humanoid:MoveTo(((me.Position - location) * Vector3.new(1, 0, 1)).Unit * 15) -- go 15 studs away from the sound
	end
end
game.ServerStorage.SoundPlayedEvent.Event:Connect(onHearSound)

The latter lets you contain all the logic for the monster inside the monster’s code, so that everything related to a monster type stays within the monster type’s script.
But the former would split up the monster logic and thus make it annoying to modify for special cases or different monster types.
It’s not really about what makes it work, but what makes it easier to work with.

5 Likes

Oh, wait it would Thank’s for the help!

I just realized how would i make it so the monster cant hear behind walls?

Make a raycast from the monster toward the sound. A raycast is a way to shoot a “ray” from a position and a direction, and check if there are any parts in that direction.
If the raycast hits nothing, then the way is clear.
If the raycast hits something, then something is in the way.
This is very literal, though. A bullet could be “in the way” if it happens to be in the right place at the right time. The ray could also “hit” the floor and think that the floor is in the way, even if the sound was right there, on the ground.

Thanks I’m just probably going to search that up because i’ve never done raycasts

origin
The origin point of the ray.

This could be me.Position

direction
The directional vector of the ray. Note that the length of this vector matters, as parts/terrain further away than its length will not be tested.

This could be location - me.Position, which makes the ray point from me to location.
If you’re confused: if you have numbers 1 and 5, then what do you add to 1 to get 5? You add 4, which is 5 - 1 (target - origin)
The length of the ray would already be the distance between me and location, because location - me.Position is already the space between the two. You can use (location - me.Position).Magnitude to see that length.

raycastParams
RaycastParams{IgnoreWater=false, CollisionGroup=Default, FilterDescendantsInstances={}}

You might use this (you have to create it with RaycastParams.new()) to filter out all monsters, bullets etc. (all non-walls) so the ray ignores them, and won’t hit the zombie itself on its way to the sound.

RaycastResult

If this is nil, then nothing was hit, and so nothing is between the zombie and the sound.
If this is something, then it’s a RaycastResult, and it can have .Instance which is what it hit, or .Position which is where it hit something. You probably don’t need any of that.

Thanks again it really helps! :slight_smile:

hello, again I just realized that how would this work on player walking bc the connects only happen once how would I make it always checking??

You’d have to fire the SoundPlayedSomewhere function regularly while the player is moving.
If your sound loops, then you can connect to the DidLoop event of the sound and alert nearby monsters each time it loops.

Run a game in Play mode (with your character) and put this in the command bar and walk around:

game.Players.LocalPlayer.Character.HumanoidRootPart.Running.DidLoop:Wait() print("aaa")

This will print “aaa” after the footstep sound has looped once.

The frequency of looping varies based on how long the sound is, which isn’t fast enough most of the time.
You can start a loop each time the player starts moving and alert monsters every second until the player is no longer moving, which makes it more consistent.
The loop must on the server side, so you need a good and reliable way of knowing when the player has started moving (to start the loop) and when it has stopped moving (to stop the loop). Player input or checking when the movement sound starts/stops is enough, but vulnerable to exploiters (which aren’t a big enough problem most of the time)

can you give a script reference i get the 1st half but the 2nd half is a bit hard for me to understand

Right, so I found something that will work for this: the Humanoid.Running event.

Play with a character again and put this in the command bar:

game.Players.LocalPlayer.Character.Humanoid.Running:Connect(print)

Now a bunch of numbers will be printed each time you start or stop running.
The important part is that the number will be 0 when you’ve stopped, and not 0 when you’re moving.

Let’s make it only print when you start running, or stop running.

-- Still in a localscript or the command bar
local isRunning = false
local humanoid = game.Players.LocalPlayer.Character.Humanoid

humanoid.Running:Connect(function(speed)
	if isRunning == false and speed ~= 0 then -- previously was standing, but now is moving
		isRunning = true
		print("Started")
		
	elseif isRunning == true and speed == 0 then -- previously was moving, but now is standing
		isRunning = false
		print("Stopped")
	end
end)

So now the script needs to start a loop when the character starts running, and stop it when the character stops running.

-- Still in a localscript or the command bar
local isRunning = false
local humanoid = game.Players.LocalPlayer.Character.Humanoid

humanoid.Running:Connect(function(speed)
	if isRunning == false and speed ~= 0 then
		isRunning = true
		-- removed
		-- added ------
		repeat
			print("Currently running")
			wait(1)
		until isRunning == false
		print("Loop stopped")
		---------------
		
	elseif isRunning == true and speed == 0 then
		isRunning = false
		print("Stopped")
	end
end)

This has a bug, though.
The loop can be started many times by spamming buttons.
There are two ways to fix that.
One is to not make another loop while it’s already looping:

Code
-- Still in a localscript or the command bar
local isRunning = false
local isLooping = false -- added
local humanoid = game.Players.LocalPlayer.Character.Humanoid

humanoid.Running:Connect(function(speed)
	if isRunning == false and speed ~= 0 then
		isRunning = true
		if isLooping == true then return end -- added
		isLooping = true -- added
		repeat
			print("Currently running")
			wait(1)
		until isRunning == false
		print("Loop stopped")
		isLooping = false -- added
		
	elseif isRunning == true and speed == 0 then
		isRunning = false
		-- removed for less spam
	end
end)

…
Another is to create an isRunning value for each individual loop. This requires some functional programming, which might go over your head if you’re new.

Code
-- Still in a localscript or the command bar
local StopRunning = nil -- changed. this will be nil while standing, or a function that stops the loop while not standing
local humanoid = game.Players.LocalPlayer.Character.Humanoid

humanoid.Running:Connect(function(speed)
	if StopRunning == nil and speed ~= 0 then
		local isRunning = true -- this is now local
		
		StopRunning = function()
			isRunning = false
			StopRunning = nil
			print("StopRunning called")
		end
		
		repeat
			print("Currently running")
			wait(1)
		until isRunning == false
		
		print("Loop stopped")
		
	elseif StopRunning ~= nil and speed == 0 then
		StopRunning()
	end
end)

There is no easy way to stop the loop “in the middle”. It will dumbly wait for 1 second and do its thing. The best you can do is hide it.
The second one is more suited for doing just that. I will be using that one as an example now.

The script is really loud, especially when the loop stops.
But it can be made a lot quieter by replacing this:

repeat
	print("Still running!")
	wait(1)
until isRunning == false

print("Loop stopped")

with:

print("Started running")
while true do
	wait(1)
	if isRunning == false then
		break
	else
		print("Currently running")
	end
end

-- remove the print after this. the loop stopping is not interesting

Now the script will print “Started running” when you start, “Currently running” every second while continuously moving and “StopRunning called” when you stop. Nothing more or less.
You can replace those prints with an action to take when starting moving, when you have moved a little and when you stop moving (you can just do nothing when the player stops moving, too). The wait(1) can be changed to make it check faster or slower.


Now it needs to call SoundPlayedSomewhere() every second.

First, put that script in a Script (not LocalScript).
The Script can go in game.StarterPlayer.StarterCharacterScripts.

When you try it out, it should error at line 2.
attempt to index nil with 'Character' - Server
It means that there is no game.Players.LocalPlayer. Only LocalScripts can have that.
I’ll leave it to you to fix that line. A hint: the script is inserted inside every new character.

The script should work again after that line is fixed, but with some lag. That’s ok and expected.

Now you need to get the SoundPlayedSomewhere() function into that script somehow.
I’d suggest just copy-pasting it into the script. You can use a ModuleScript, but it’s not worth it for such a small function.

Then all you need to do is use that function in the loop.
The wait() can be moved below it if the sound should be played as soon as the player starts moving. Otherwise, people can tippy-toe around without alerting monsters by spam pressing the movement keys.

Speaking of just that…
If you want slow walkers or mobile users to also tip-toe, you can make the stop/start running checks not trigger if speed isn’t high enough.
Currently it considers anything higher than 0 to be running.
Change these lines to make monsters ignore anyone that’s running at less than 8 studs per second:

	if StopRunning == nil and speed ~= 0 then
	elseif StopRunning ~= nil and speed == 0 then

…

	if StopRunning == nil and speed >= 8 then
	elseif StopRunning ~= nil and speed < 8 then

Just putting this here because it’s easy to do.

A few things on some of them the character does not load fast enough fast fix , but how exactly would i send this info to the enemy bc i would think events but you said something about module scripts?

It’s already done using events in another script:

Copy it to the top or bottom of the script and use the function to alert enemies.

Nothing actually uses the sound, so you can just put nil instead of it.

SoundPlayedSomewhere(nil, current-position-of-the-player)