Parent changes never being detected

i have a script for an entity that floats toward the player, and it works fine. the problem arises when i try to detect a change in the entity’s parent (from ReplicatedStorage to the workspace) to trigger an ambient sound within the HumanoidRootPart. the detection function never gets called. all the necessary children are present, no infinite yield warnings, and no errors in the output. i ran into this issue while trying to clone these entities. it’s been a real head-scratcher.

the script that clones the models and parents them into the workspace
local Players = game:GetService("Players") -- unused for now
local ReplicatedStorage = game:GetService("ReplicatedStorage")

local creatures = ReplicatedStorage:FindFirstChild("evil creatures")
local creatureSpawns = workspace:FindFirstChild("evil creature spawns")
local remoteFolder = ReplicatedStorage:FindFirstChild("remotes") -- remotes for later

function getRandomChild(parent)
	local children = parent:GetChildren()
	local randomChild = math.random(1, #children)

	if children[randomChild] then
		return children[randomChild] -- return the child instance, not the index
	end

	return nil
end

function wakeCreatures(creatureNum)
	for i = 1, creatureNum do
		local randomCreature = getRandomChild(creatures)
		local randomSpawn = getRandomChild(creatureSpawns)
		
		if randomCreature then
			local newCreature = randomCreature:Clone()
			newCreature.Parent = workspace
			newCreature:PivotTo(randomSpawn.CFrame)
		end
	end
end

wakeCreatures(math.random(38, 64)) -- wakey wakey
the entity script:
local PathfindingService = game:GetService("PathfindingService")
local Players = game:GetService("Players")
local RunService = game:GetService("RunService")

local goober = script.Parent
local personoid = goober:FindFirstChildWhichIsA("Humanoid")
local root = goober:FindFirstChild("HumanoidRootPart")
local bodyForce = root:FindFirstChildWhichIsA("BodyForce")
local bodyPos = root:FindFirstChildWhichIsA("BodyPosition")
local ambientSound = root:FindFirstChild("the him...")
local oldParent = goober.Parent

local waypoints
local nextWaypointIndex
local currentTime = 0
local path = PathfindingService:CreatePath()

-- stuck detection variables
local stuckCheckTime = 5 -- number of seconds before considering it stuck
local lastPosition = root.Position
local stuckTimer = 0
local isStuck = false

personoid:ChangeState(Enum.HumanoidStateType.Physics) -- yummers

local constants = {
	DEBUG_MODE = false;

	ROCKET_FIRE_CHANCE = 0.11;
	SCAN_DIST = 1250;
	UPDATE_TIME = 0.5;
	WAYPOINT_INTERVAL = 4;

	PATHING_DATA = {
		D = 1550,
		MaxForce = Vector3.one * 23,
		P = 17000,
	};

	MOVEMENT_DATA = {
		D = 1250,
		MaxForce = Vector3.one * 3,
		P = 10000,
	};
}

-- utility functions
local function getDistance(a, b)
	return (a - b).Magnitude
end

local function updateForce()
	bodyForce.Force = Vector3.new(0, root:GetMass() * workspace.Gravity - 5, 0)
end

local function checkParent() -- does this never fire? nothing is ever printed
	local newParent = goober.Parent
	print(`checking parent... old parent: {oldParent}, new parent: {newParent}`)

	if oldParent ~= newParent then
		print(`parent changed`)
		ambientSound:Play()
		oldParent = newParent
	end	
end

-- target finding + movement
local function getNearestRoot()
	local nearestChar = nil
	local shortestDist = constants.SCAN_DIST

	for _, player in Players:GetPlayers() do
		local character = player.Character

		if character and character:FindFirstChild("HumanoidRootPart") then
			local plrRoot = character.HumanoidRootPart
			local charPos = plrRoot.Position
			local dist = getDistance(charPos, root.Position)

			if dist < shortestDist then
				shortestDist = dist
				nearestChar = character
			end
		end
	end

	return nearestChar and nearestChar:FindFirstChild("HumanoidRootPart")
end

local function performRaycast()
	local plrRoot = getNearestRoot()

	if plrRoot then
		local direction = (plrRoot.Position - root.Position)

		if direction.Magnitude > constants.SCAN_DIST then
			direction = direction.Unit * constants.SCAN_DIST
		end

		local balloonParams = RaycastParams.new()
		balloonParams.RespectCanCollide = true
		balloonParams.FilterType = Enum.RaycastFilterType.Exclude
		balloonParams.FilterDescendantsInstances = {goober, plrRoot.Parent}

		local result = workspace:Raycast(root.Position, direction, balloonParams)
		return result ~= nil
	end

	return false
end

local function adjustForGround(position)
	local rayOrigin = position + Vector3.new(0, 10, 0)
	local rayDirection = Vector3.new(0, -50, 0)

	local params = RaycastParams.new()
	params.FilterType = Enum.RaycastFilterType.Exclude
	params.FilterDescendantsInstances = {goober} -- prevent it from floating upwards

	local result = workspace:Raycast(rayOrigin, rayDirection, params)

	if result then
		return Vector3.new(position.X, result.Position.Y + 3.5, position.Z)
	else
		return position + Vector3.new(0, 3.5, 0)
	end
end

local function runPath()
	root:SetNetworkOwner(nil)
	local nearestRoot = getNearestRoot()

	if nearestRoot then
		path:ComputeAsync(root.Position, nearestRoot.Position)
		waypoints = path:GetWaypoints()
		nextWaypointIndex = 2

		if #waypoints < 2 or path.Status == Enum.PathStatus.NoPath then
			bodyPos.Position = nearestRoot.Position
			return
		end

		if nextWaypointIndex <= #waypoints then
			local waypoint = waypoints[nextWaypointIndex]
			if (waypoint.Position - root.Position).Magnitude < 5 then
				nextWaypointIndex += constants.WAYPOINT_INTERVAL
			end

			if nextWaypointIndex > #waypoints then
				nextWaypointIndex = #waypoints
			end

			local adjustedPosition = adjustForGround(waypoints[nextWaypointIndex].Position)
			bodyPos.Position = adjustedPosition
		end
	end
end

local function checkIfStuck(dt)
	local distanceMoved = (root.Position - lastPosition).Magnitude

	if distanceMoved < 0.2 then
		stuckTimer += dt
	else
		stuckTimer = 0
	end

	lastPosition = root.Position

	if stuckTimer >= stuckCheckTime then
		isStuck = true
		local behindPosition = root.CFrame.Position - root.CFrame.LookVector * 6
		root:ApplyImpulse((behindPosition * Vector3.new(math.random(-4, 4), math.random(-5, -2), math.random(-4, 4))) / 11)
	else
		isStuck = false
	end
end

local function floatTowardsPlayer()
	local shouldPathfind = performRaycast()
	local nearestRoot = getNearestRoot()

	if nearestRoot then
		if shouldPathfind then
			runPath()
			for key, value in pairs(constants.PATHING_DATA) do
				bodyPos[key] = value
			end
		else
			bodyPos.Position = nearestRoot.Position
			for key, value in pairs(constants.MOVEMENT_DATA) do
				bodyPos[key] = value
			end
		end
	else
		bodyPos.Position = root.Position
	end
end

-- why does this not work (the model is being cloned, but this script is ALSO being cloned)
goober:GetPropertyChangedSignal("Parent"):Connect(function()
	print(`calling parent change function`)
	checkParent()
end)

-- runtime
updateForce()
workspace:GetPropertyChangedSignal("Gravity"):Connect(updateForce)
root:GetPropertyChangedSignal("Mass"):Connect(updateForce)

RunService.Heartbeat:Connect(function(dt)
	currentTime += dt
	checkIfStuck(dt) -- hello gordon

	if currentTime >= constants.UPDATE_TIME then
		currentTime = 0
		floatTowardsPlayer()
	end
end)

When you clone an object its parent is nil.
image

You problem comes from the fact that before the entity script has even loaded the entity is already parented under the workspace. At the bottom of the entity code just put the sound there and it should work without the need to check if the parent is changing.

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.