311 lines
8.2 KiB
Lua
311 lines
8.2 KiB
Lua
-- Roblox character sound script
|
|
|
|
local Players = game:GetService("Players")
|
|
local RunService = game:GetService("RunService")
|
|
|
|
local SOUND_DATA = {
|
|
Climbing = {
|
|
SoundId = "rbxasset://sounds/action_footsteps_plastic.mp3",
|
|
Looped = true,
|
|
},
|
|
Died = {
|
|
SoundId = "rbxasset://sounds/uuhhh.mp3",
|
|
},
|
|
FreeFalling = {
|
|
SoundId = "rbxasset://sounds/action_falling.mp3",
|
|
Looped = true,
|
|
},
|
|
GettingUp = {
|
|
SoundId = "rbxasset://sounds/action_get_up.mp3",
|
|
},
|
|
Jumping = {
|
|
SoundId = "rbxasset://sounds/action_jump.mp3",
|
|
},
|
|
Landing = {
|
|
SoundId = "rbxasset://sounds/action_jump_land.mp3",
|
|
},
|
|
Running = {
|
|
SoundId = "rbxasset://sounds/action_footsteps_plastic.mp3",
|
|
Looped = true,
|
|
Pitch = 1.85,
|
|
},
|
|
Splash = {
|
|
SoundId = "rbxasset://sounds/impact_water.mp3",
|
|
},
|
|
Swimming = {
|
|
SoundId = "rbxasset://sounds/action_swim.mp3",
|
|
Looped = true,
|
|
Pitch = 1.6,
|
|
},
|
|
}
|
|
|
|
-- wait for the first of the passed signals to fire
|
|
local function waitForFirst(...)
|
|
local shunt = Instance.new("BindableEvent")
|
|
local slots = {...}
|
|
|
|
local function fire(...)
|
|
for i = 1, #slots do
|
|
slots[i]:Disconnect()
|
|
end
|
|
|
|
return shunt:Fire(...)
|
|
end
|
|
|
|
for i = 1, #slots do
|
|
slots[i] = slots[i]:Connect(fire)
|
|
end
|
|
|
|
return shunt.Event:Wait()
|
|
end
|
|
|
|
-- map a value from one range to another
|
|
local function map(x, inMin, inMax, outMin, outMax)
|
|
return (x - inMin)*(outMax - outMin)/(inMax - inMin) + outMin
|
|
end
|
|
|
|
local function playSound(sound)
|
|
sound.TimePosition = 0
|
|
sound.Playing = true
|
|
end
|
|
|
|
local function shallowCopy(t)
|
|
local out = {}
|
|
for k, v in pairs(t) do
|
|
out[k] = v
|
|
end
|
|
return out
|
|
end
|
|
|
|
local function initializeSoundSystem(player, humanoid, rootPart)
|
|
local sounds = {}
|
|
|
|
-- initialize sounds
|
|
for name, props in pairs(SOUND_DATA) do
|
|
local sound = Instance.new("Sound")
|
|
sound.Name = name
|
|
|
|
-- set default values
|
|
sound.Archivable = false
|
|
sound.EmitterSize = 5
|
|
sound.MaxDistance = 150
|
|
sound.Volume = 0.65
|
|
|
|
for propName, propValue in pairs(props) do
|
|
sound[propName] = propValue
|
|
end
|
|
|
|
sound.Parent = rootPart
|
|
sounds[name] = sound
|
|
end
|
|
|
|
local playingLoopedSounds = {}
|
|
|
|
local function stopPlayingLoopedSounds(except)
|
|
for sound in pairs(shallowCopy(playingLoopedSounds)) do
|
|
if sound ~= except then
|
|
sound.Playing = false
|
|
playingLoopedSounds[sound] = nil
|
|
end
|
|
end
|
|
end
|
|
|
|
-- state transition callbacks
|
|
local stateTransitions = {
|
|
[Enum.HumanoidStateType.FallingDown] = function()
|
|
stopPlayingLoopedSounds()
|
|
end,
|
|
|
|
[Enum.HumanoidStateType.GettingUp] = function()
|
|
stopPlayingLoopedSounds()
|
|
playSound(sounds.GettingUp)
|
|
end,
|
|
|
|
[Enum.HumanoidStateType.Jumping] = function()
|
|
stopPlayingLoopedSounds()
|
|
playSound(sounds.Jumping)
|
|
end,
|
|
|
|
[Enum.HumanoidStateType.Swimming] = function()
|
|
local verticalSpeed = math.abs(rootPart.Velocity.Y)
|
|
if verticalSpeed > 0.1 then
|
|
sounds.Splash.Volume = math.clamp(map(verticalSpeed, 100, 350, 0.28, 1), 0, 1)
|
|
playSound(sounds.Splash)
|
|
end
|
|
stopPlayingLoopedSounds(sounds.Swimming)
|
|
sounds.Swimming.Playing = true
|
|
playingLoopedSounds[sounds.Swimming] = true
|
|
end,
|
|
|
|
[Enum.HumanoidStateType.Freefall] = function()
|
|
sounds.FreeFalling.Volume = 0
|
|
stopPlayingLoopedSounds(sounds.FreeFalling)
|
|
playingLoopedSounds[sounds.FreeFalling] = true
|
|
end,
|
|
|
|
[Enum.HumanoidStateType.Landed] = function()
|
|
stopPlayingLoopedSounds()
|
|
local verticalSpeed = math.abs(rootPart.Velocity.Y)
|
|
if verticalSpeed > 75 then
|
|
sounds.Landing.Volume = math.clamp(map(verticalSpeed, 50, 100, 0, 1), 0, 1)
|
|
playSound(sounds.Landing)
|
|
end
|
|
end,
|
|
|
|
[Enum.HumanoidStateType.Running] = function()
|
|
stopPlayingLoopedSounds(sounds.Running)
|
|
sounds.Running.Playing = true
|
|
playingLoopedSounds[sounds.Running] = true
|
|
end,
|
|
|
|
[Enum.HumanoidStateType.Climbing] = function()
|
|
local sound = sounds.Climbing
|
|
if math.abs(rootPart.Velocity.Y) > 0.1 then
|
|
sound.Playing = true
|
|
stopPlayingLoopedSounds(sound)
|
|
else
|
|
stopPlayingLoopedSounds()
|
|
end
|
|
playingLoopedSounds[sound] = true
|
|
end,
|
|
|
|
[Enum.HumanoidStateType.Seated] = function()
|
|
stopPlayingLoopedSounds()
|
|
end,
|
|
|
|
[Enum.HumanoidStateType.Dead] = function()
|
|
stopPlayingLoopedSounds()
|
|
playSound(sounds.Died)
|
|
end,
|
|
}
|
|
|
|
-- updaters for looped sounds
|
|
local loopedSoundUpdaters = {
|
|
[sounds.Climbing] = function(dt, sound, vel)
|
|
sound.Playing = vel.Magnitude > 0.1
|
|
end,
|
|
|
|
[sounds.FreeFalling] = function(dt, sound, vel)
|
|
if vel.Magnitude > 75 then
|
|
sound.Volume = math.clamp(sound.Volume + 0.9*dt, 0, 1)
|
|
else
|
|
sound.Volume = 0
|
|
end
|
|
end,
|
|
|
|
[sounds.Running] = function(dt, sound, vel)
|
|
sound.Playing = vel.Magnitude > 0.5 and humanoid.MoveDirection.Magnitude > 0.5
|
|
end,
|
|
}
|
|
|
|
-- state substitutions to avoid duplicating entries in the state table
|
|
local stateRemap = {
|
|
[Enum.HumanoidStateType.RunningNoPhysics] = Enum.HumanoidStateType.Running,
|
|
}
|
|
|
|
local activeState = stateRemap[humanoid:GetState()] or humanoid:GetState()
|
|
|
|
local stateChangedConn = humanoid.StateChanged:Connect(function(_, state)
|
|
state = stateRemap[state] or state
|
|
|
|
if state ~= activeState then
|
|
local transitionFunc = stateTransitions[state]
|
|
|
|
if transitionFunc then
|
|
transitionFunc()
|
|
end
|
|
|
|
activeState = state
|
|
end
|
|
end)
|
|
|
|
local steppedConn = RunService.Stepped:Connect(function(_, worldDt)
|
|
-- update looped sounds on stepped
|
|
for sound in pairs(playingLoopedSounds) do
|
|
local updater = loopedSoundUpdaters[sound]
|
|
|
|
if updater then
|
|
updater(worldDt, sound, rootPart.Velocity)
|
|
end
|
|
end
|
|
end)
|
|
|
|
local humanoidAncestryChangedConn
|
|
local rootPartAncestryChangedConn
|
|
local characterAddedConn
|
|
|
|
local function terminate()
|
|
stateChangedConn:Disconnect()
|
|
steppedConn:Disconnect()
|
|
humanoidAncestryChangedConn:Disconnect()
|
|
rootPartAncestryChangedConn:Disconnect()
|
|
characterAddedConn:Disconnect()
|
|
end
|
|
|
|
humanoidAncestryChangedConn = humanoid.AncestryChanged:Connect(function(_, parent)
|
|
if not parent then
|
|
terminate()
|
|
end
|
|
end)
|
|
|
|
rootPartAncestryChangedConn = rootPart.AncestryChanged:Connect(function(_, parent)
|
|
if not parent then
|
|
terminate()
|
|
end
|
|
end)
|
|
|
|
characterAddedConn = player.CharacterAdded:Connect(terminate)
|
|
end
|
|
|
|
local function playerAdded(player)
|
|
local function characterAdded(character)
|
|
-- Avoiding memory leaks in the face of Character/Humanoid/RootPart lifetime has a few complications:
|
|
-- * character deparenting is a Remove instead of a Destroy, so signals are not cleaned up automatically.
|
|
-- ** must use a waitForFirst on everything and listen for hierarchy changes.
|
|
-- * the character might not be in the dm by the time CharacterAdded fires
|
|
-- ** constantly check consistency with player.Character and abort if CharacterAdded is fired again
|
|
-- * Humanoid may not exist immediately, and by the time it's inserted the character might be deparented.
|
|
-- * RootPart probably won't exist immediately.
|
|
-- ** by the time RootPart is inserted and Humanoid.RootPart is set, the character or the humanoid might be deparented.
|
|
|
|
if not character.Parent then
|
|
waitForFirst(character.AncestryChanged, player.CharacterAdded)
|
|
end
|
|
|
|
if player.Character ~= character or not character.Parent then
|
|
return
|
|
end
|
|
|
|
local humanoid = character:FindFirstChildOfClass("Humanoid")
|
|
while character:IsDescendantOf(game) and not humanoid do
|
|
waitForFirst(character.ChildAdded, character.AncestryChanged, player.CharacterAdded)
|
|
humanoid = character:FindFirstChildOfClass("Humanoid")
|
|
end
|
|
|
|
if player.Character ~= character or not character:IsDescendantOf(game) then
|
|
return
|
|
end
|
|
|
|
-- must rely on HumanoidRootPart naming because Humanoid.RootPart does not fire changed signals
|
|
local rootPart = character:FindFirstChild("HumanoidRootPart")
|
|
while character:IsDescendantOf(game) and not rootPart do
|
|
waitForFirst(character.ChildAdded, character.AncestryChanged, humanoid.AncestryChanged, player.CharacterAdded)
|
|
rootPart = character:FindFirstChild("HumanoidRootPart")
|
|
end
|
|
|
|
if rootPart and humanoid:IsDescendantOf(game) and character:IsDescendantOf(game) and player.Character == character then
|
|
initializeSoundSystem(player, humanoid, rootPart)
|
|
end
|
|
end
|
|
|
|
if player.Character then
|
|
characterAdded(player.Character)
|
|
end
|
|
player.CharacterAdded:Connect(characterAdded)
|
|
end
|
|
|
|
Players.PlayerAdded:Connect(playerAdded)
|
|
for _, player in ipairs(Players:GetPlayers()) do
|
|
playerAdded(player)
|
|
end
|