How can I make a model appear LOCALLY while touching a part?

Hello everyone!

I’m back here again because something doesn’t work :sweat_smile:

Here’s the issue:

A game I’m working on has a shop, and when the player enters the shop I want a random shopkeeper to appear which is an avatar model, but only locally. The avatars are stored in a folder in workspace called ShopNPCs.

I have an invisible and cancollide = false hitbox in the shop, and I’m trying to make the random NPC appear when the player touches the box, and disappear when they leave.

Here’s the code (localscript):

local hitBox = script.Parent

function disappear()
	for _, v in NPC:GetChildren() do
		if v:IsA("Part") then
			if v.Name ~= "HumanoidRootPart" then
				v.Transparency = 1
			end
		end
		if v:IsA("Accessory") then
			for _, v in v:GetChildren() do
				if v.Name == "Handle" then
					v.Transparency = 1
				end
			end
		end
		NPC.Head.face.Transparency = 1
	end
end


function reappear()
	for _, v in NPC:GetChildren() do
		if v:IsA("Part") then
			if v.Name ~= "HumanoidRootPart" then
				v.Transparency = 0
			end
		end
		if v:IsA("Accessory") then
			for _, v in v:GetChildren() do
				if v.Name == "Handle" then
					v.Transparency = 0
				end
			end
		end
		NPC.Head.face.Transparency = 0
	end
end





hitBox.Touched:Connect(function(part)
	local player = game.Players.LocalPlayer
	local randomnum = math.random(0, 1)
	if randomnum == 0 then
		NPC = game.Workspace.ShopNPCs.NPC1
	end
	if randomnum == 1 then
		NPC = game.Workspace.ShopNPCs.NPC2
	end
	if player and player.Character and player.Character.PrimaryPart == part then
		reappear()
	end
end)

hitBox.TouchEnded:Connect(function(part)
	local player = game.Players.LocalPlayer
	if player and player.Character and player.Character.PrimaryPart == part then
		disappear()
	end
end)

Unfortunately, it doesn’t work.
Can anyone help me out here? I’d greatly appreciate it!

(P.S. The players enter and leave the shop via a character.HumanoidRootPart.CFrame = CFrame.new(partToTeleportTo.Position) command after a proximityprompt is activated. Just in case that’s relevant.)

Thanks in advance!

~innit_2winnit

I’m pretty sure you need to store the NPC in ServerStorage then you can just clone it from there.
Saves the for _,v loops
Then you can just destroy the clone.

Also you can clean up your code with

	if randomnum == 0 then
		NPC = game.Workspace.ShopNPCs.NPC1 --and reset this path link to the ServerStorage file.
    else
		NPC = game.Workspace.ShopNPCs.NPC2 -- see above.
	end

I don’t think this script is running at all. Local scripts have limited places to run. You can use “Script” (not a local script) and set “RunContext” to “Client”.

You can try this code but I didn’t test it in studio:

local Players = game:GetService("Players")
local npcs = {game.Workspace:WaitForChild("ShopNPCs"):WaitForChild("NPC1"), game.Workspace:WaitForChild("ShopNPCs"):WaitForChild("NPC2")}
local hitBox = script.Parent
local localPlayer = Players.LocalPlayer
local visible = false

local function hideNpc()
	for _, npc in npcs do
		for _, part in npc:GetDescendants() do
			if not part:IsA("BasePart") or part.Name == "HumanoidRootPart" then continue end
			part.Transparency = 1
		end
		NPC.Head.face.Transparency = 1
	end
end

local function showNpc()
	local npc = npcs[math.random(1, #npcs)]
	for _, part in npc:GetDescendants() do
		if not part:IsA("BasePart") or part.Name == "HumanoidRootPart" then continue end
		part.Transparency = 0
	end
	NPC.Head.face.Transparency = 0
end

hitBox.Touched:Connect(function(part)
	local player = Players:GetPlayerFromCharacter(part:FindFirstAncestorOfClass("Model"))
	if not player or not player == localPlayer or visible then return end
	
	showNpc()
	visible = true
end)

hitBox.TouchEnded:Connect(function(part)
	local player = Players:GetPlayerFromCharacter(part:FindFirstAncestorOfClass("Model"))
	if not player or not player == localPlayer or not visible then return end
	
	hideNpc()
	visible = false
end)

I will use both ideas tomorrow and get back to you then! Stay tuned and thank you for replying :slight_smile:

So I got it to work; partially. I used the clone() idea along with the serverscript runcontext being client. Here’s my code:

local hitBox = script.Parent
local isvis = false

hitBox.Touched:Connect(function(part)
	local player = game.Players.LocalPlayer
	if player and player.Character and player.Character.PrimaryPart == part then
		local randomnum = math.random(0, 1)
		if isvis == false then
			if randomnum == 0 then
				NPC = game.ReplicatedStorage.ShopNPCs.npc1:Clone()
				NPC.Parent = workspace
				isvis = true
			end
			if randomnum == 1 then
				NPC = game.ReplicatedStorage.ShopNPCs.npc2:Clone()
				NPC.Parent = workspace
				isvis = true
			end
		end
	end	
end)

hitBox.TouchEnded:Connect(function(part)
	if isvis == true then
		NPC:Destroy()
		isvis = false
	end
end)


The only issue is that when the player jumps, the shop NPC changes. How can I prevent this?

All the best,

~innit_2winnit

Touched can be really finicky, especially for Humanoids.
It can register hits each time the player moves.

I’m aware of that. Is there any way to prevent this?

Debounces will fix this

Debounces = {} 

part.Touched:connect(function(player)
   if not Debounces[player] then
       Debounces[player] = true 
       print("Touched")
       task.wait(2)
       Debounces[player] = nil
   end
end)

Or you can do this, this will only fire once. When player touches the Part.
And will only fire again, once the player is no longer touching the Part

local AlreadyTouched = {}

part.Touched:connect(function(player)
   if not AlreadyTouched[player] then
        AlreadyTouched[player] = true
   end
end)

part.TouchEnded:connect(function(player)
    AlreadyTouched[player] = nil 
end)

Adding onto this, since the Touched and TouchEnded connections are temperamental, and dont fire sometimes. You could do this


local AlreadyTouched = {}
local RunService = game:GetService("RunService")

part.Touched:connect(function(player)
	if not AlreadyTouched[player] then
		AlreadyTouched[player] = true
		local RunServiceConnection
		
		local overlap = OverlapParams.new()
		overlap.FilterDescendantsInstances = {player}
		overlap.FilterType = Enum.RaycastFilterType.Include
		
		local function checkStatus()
			local Parts = game.Workspace:GetPartsInPart(part,overlap)
			if #Parts <= 0 then
				AlreadyTouched[player] = nil 
				RunServiceConnection:Disconnect()
			end
		end
		
		RunServiceConnection =  RunService.Heartbeat:Connect(checkStatus)
	end
end)
1 Like

I was actually able to fix this problem using a random post I found on the devforum.

Here’s my code (in a serverscript where runcontext is client):

local hitBox = script.Parent
local isvis = false

hitBox.Touched:Connect(function(h)
	if h and h.Parent:FindFirstChild("Humanoid") then
		if isvis == false then
			local randomnum = math.random(0, 1)
			if randomnum == 0 then
				NPC = game.ReplicatedStorage.ShopNPCs.npc1:Clone()
				NPC.Parent = workspace
				isvis = true
				print("npc1")
			end
			if randomnum == 1 then
				isvis = true
				NPC = game.ReplicatedStorage.ShopNPCs.npc2:Clone()
				NPC.Parent = workspace
				print("npc2 is shopkeeper")
			end
		end
		repeat wait() until game.Players:GetPlayerFromCharacter(h.Parent):DistanceFromCharacter(hitBox.Position) > 70
			NPC:Destroy()
			isvis = false
			wait(1)
			print("NPC destroyed!")
		
	end
	
end)

The only thing I will say is that it prints the NPC destroyed! message like, 7 times whenever I leave the shop using the teleporter, but I don’t think it will cause the issue (maybe clogging up the output possibly).

Any feedback on this is welcome (just in case this is secretly gonna cause the worst performance issues or something :sweat_smile:)

All the best,

~innit_2winnit

I wouldn’t use this code :grimacing: :grimacing: This will cause lag. As it creates a loop every time the Touched event is fired. (Which will happen alot if the player is moving ever so slighty while in the Hit Box of the Part)

I combined your script with mine.

local AlreadyTouched = {}
local RunService = game:GetService("RunService")

local isvis = false

hitBox.Touched:connect(function(part)
	local Humanoid = part.Parent:FindFirstChild("Humanoid")
	if Humanoid then
		if not AlreadyTouched[Humanoid] then
			local player = Humanoid.Parent 
			
			AlreadyTouched[Humanoid] = true
			
			if isvis == false then
				local randomnum = math.random(0, 1)
				if randomnum == 0 then
					NPC = game.ReplicatedStorage.ShopNPCs.npc1:Clone()
					NPC.Parent = workspace
					isvis = true
					print("npc1")
				end
				if randomnum == 1 then
					isvis = true
					NPC = game.ReplicatedStorage.ShopNPCs.npc2:Clone()
					NPC.Parent = workspace
					print("npc2 is shopkeeper")
				end
			end 
			
			local function PlayerUnTouched() 
				NPC:Destroy()
				isvis = false 
			end
			
			local RunServiceConnection

			local overlap = OverlapParams.new()
			overlap.FilterDescendantsInstances = {player}
			overlap.FilterType = Enum.RaycastFilterType.Include

			local function checkStatus()
				local Parts = game.Workspace:GetPartsInPart(hitBox,overlap)
				if #Parts <= 0 then
					PlayerUnTouched()
					AlreadyTouched[Humanoid] = nil 
					RunServiceConnection:Disconnect()
				end
			end

			RunServiceConnection =  RunService.Heartbeat:Connect(checkStatus)
		end
	end
end)```

OH OOPS I didnt see you had a Debounce of sorts in this script with the variable:
isvis

All good :slightly_smiling_face:

I will probably make use of the code you provided tomorrow as I assume it’s more efficient than mine. Thank you for your help, I really appreciate it!

Good luck with your future projects and all the best,

~innit_2winnit

1 Like

I’m not sure about more efficient. My code doesn’t need to calculate distances which might help performance, idrk? But it is more precise as it would destroy as soon as player leaves the part

Ah, I see.

Instantaneous destruction doesn’t matter because the door to the shop takes like 4 seconds to exit and then renter, so there is a bit of leeway with the destruction.

I won’t change my code but I will mark yours as the solution for being the “best practice” :slight_smile:

Thanks for all your help!!

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