MouseCastPlus v1.5 | Easily detect 3D mouse interactions

:vhs:Source Code | :ocean:Playground

Mousecast Plus is a versatile module that simplifies 3D mouse detection with raycasting. It provides seamless functionality through onHoverEnter, onHoverLeave, and onClick events, enabling intuitive interactions with 3D objects. Designed for compatibility across all platforms, Mousecast Plus makes implementing advanced mouse interactions effortless.

📖Tutorial/Documentation

How to require MouseCastPlus

local MouseCastPlus = require(game.ReplicatedStorage.MouseCastPlus) -- where you keep mousecastplus

How To Use

MouseCastPlus.new({table})
Function that creates a new table of parts to do the effect on.

local hoverObjects = MouseCastPlus.new({ Basepart, Basepart }, Maxactivationdisatance) -- in the table is where you will put all of the parts, you can have one or more parts in this table. For maxactivationdistance you can leave it blank for infinite distance, or a number such as 20 studs away.

hoverObject:onHoverEnter(function(part)

hoverObject:onHoverEnter(function(part)
        -- what you want to happen when mouse enters/hovers
end)

hoverObject:onHoverLeave(function(part)

hoverObject:onHoverLeave(function(part)
        -- what you want to happen when the mouse leaves
end)

hoverObject:onClick(function(part)

hoverObject:onClick(function(part)
       -- what you want to happen when the player clicks on the part
end)

How to delete instances and events

hoverObject:Destroy()

How to disconnect/temporarily disable the system

HoverObjects:Disconnect()

How to reconnect the system

HoverObjects:Connect()

:movie_camera:Showcases

:computer_mouse: Hover Example


:door:Door System

Credits

@JJsStuff_1 → Created the system and dev-forum
@ForeverHD → When uploading this dev-forum post, I used his Zone-Plus post as reference for how I should format this.

Final

MouseCast Plus is free and open source. If you have any questions or suggestions I am all ears :ear: or I guess eyes :eyes:

You are welcome to use and modify this module for any of your projects, credits would be greatly appreciated but not required :pray: :smile:

Please do not reupload this module unless you made changes and its not a virus.

That is the module, thank you :pray:

18 Likes

This module is amazing! I just have one question…

Ive tried to modularize it by doing this:

--//Services
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local TweenService = game:GetService("TweenService")
local CollectionService = game:GetService("CollectionService")

--//Modules
local Modules = ReplicatedStorage.Modules

local MouseCastPlus = require(Modules.UI.Components.MouseCast)

--//Tagged
local ThreeDHiglightUITagged = CollectionService:GetTagged("3DHighlightUI")
local ThreeDColorUITagged = CollectionService:GetTagged("3DColorUI")
local ThreeDDoorUITagged = CollectionService:GetTagged("3DDoorUI")

--//Main 
local Knit = require(ReplicatedStorage.Packages.Knit)

--//Module
local ThreeDInteractionController = Knit.CreateController  {
	Name = "ThreeDInteractionController",
}

--//Start
function ThreeDInteractionController:KnitStart()
	
	for _, Object in ipairs(ThreeDHiglightUITagged) do
		task.spawn(HandleHighlight, Object)
	end

	for _, Object in ipairs(ThreeDColorUITagged) do
		task.spawn(HandleColor, Object)
	end

	for _, Object in ipairs(ThreeDDoorUITagged) do
		task.spawn(HandleDoors, Object)
	end
	
	
end

--//Methods
function HandleHighlight(Object)
	local hoverObject = MouseCastPlus.new(Object)

	hoverObject:onHoverEnter(function(part)
		local highlight = part:FindFirstChildOfClass("Highlight")
		if highlight then
			TweenService:Create(highlight, TweenInfo.new(0.25), {OutlineTransparency = 0}):Play()
		end
	end)

	hoverObject:onHoverLeave(function(part)
		local highlight = part:FindFirstChildOfClass("Highlight")
		if highlight then
			TweenService:Create(highlight, TweenInfo.new(0.25), {OutlineTransparency = 1}):Play()
		end
	end)

	hoverObject:onClick(function(part)
		if part:GetAttribute("Enabled") == nil then
			part:SetAttribute("Enabled", false)
		end

		local highlight = part:FindFirstChildOfClass("Highlight")

		if part:GetAttribute("Enabled") == true then
			part:SetAttribute("Enabled", false)

			if highlight then
				TweenService:Create(highlight, TweenInfo.new(0.25), {FillTransparency = 1}):Play()
			end
		else
			part:SetAttribute("Enabled", true)

			if highlight then
				TweenService:Create(highlight, TweenInfo.new(0.25), {FillTransparency = 0.5}):Play()
			end
		end
	end)
end

function HandleColor(Object)
	local hoverObject = MouseCastPlus.new(Object) -- create new parts

	hoverObject:onHoverEnter(function(part) -- mouse hovers over the object
		local tween = TweenService:Create(part, TweenInfo.new(0.25), {Color = Color3.new(0.333333, 1, 0.498039)}) -- green
		tween:Play()
	end)

	hoverObject:onHoverLeave(function(part) -- mouse leaves
		local tween = TweenService:Create(part, TweenInfo.new(0.25), {Color = Color3.new(1, 0, 0)}) -- red
		tween:Play()
	end)

	hoverObject:onClick(function(part) -- mouse clicks
		local tween = TweenService:Create(part, TweenInfo.new(0.25), {Color = Color3.new(0.333333, 0.666667, 1)}) -- blue
		tween:Play()
	end)
end

function HandleDoors(Object)
	local hoverObject = MouseCastPlus.new(Object, 20)

	hoverObject:onHoverEnter(function(part)
		local highlight = part.Parent:FindFirstChildOfClass("Highlight")
		if highlight then
			TweenService:Create(highlight, TweenInfo.new(0.25), {OutlineTransparency = 0}):Play()
		end
	end)

	hoverObject:onHoverLeave(function(part)
		local highlight = part.Parent:FindFirstChildOfClass("Highlight")
		if highlight then
			TweenService:Create(highlight, TweenInfo.new(0.25), {OutlineTransparency = 1}):Play()
		end
	end)

	hoverObject:onClick(function(door)
		if door.Parent:FindFirstChild("OpenEvent") then
			door.Parent.OpenEvent:FireServer()
		else
			warn("No remote found")
		end
	end)
end

return ThreeDInteractionController

To sumarize, Ive created functions that use a variable representing objects.

I get objects that are tagged with certain tags and do a for loop, using task.spawn to cycle through all of them.

The problem is that its not very reliable. It will often not work and sometimes it will. Could you point me towards the right direction?

1 Like

Thank you, I see that you are using Knit Framework! The game I originally made this module for runs completely off knit. What I would do differently is adding in some sort of listener

for example:

CollectionService:GetInstanceAddedSignal("3DHighlightUI"):Connect(function(newObject)
    task.spawn(HandleHighlight, newObject)
end)

This will dynamically update when parts are changed/added.

Another one of my recommendations would be adding in an attribute system like what you did with the enabling, if my previous recommendation fails.

for example:

local hoverObject = MouseCastPlus.new(Object)
Object:SetAttribute("HoverActive", false)

hoverObject:onHoverEnter(function(part)
    if not Object:GetAttribute("HoverActive") then
        Object:SetAttribute("HoverActive", true)
        -- logic for enter
    end
end)

hoverObject:onHoverLeave(function(part)
    if Object:GetAttribute("HoverActive") then
        Object:SetAttribute("HoverActive", false)
        -- logic for leave
    end
end)

Thank you for using my module! Tell me if these recommendations help at all! I will quickly show some of the code I wrote for when I used knit for my system, in summary, I created a system that allows you to choose a card, each card is a different monster with different advantages and disadvantages.

For the KnitInit function i wrote:

function MonsterController:KnitInit()
	RoundService = Knit.GetService("RoundService")
	MonsterService = Knit.GetService("MonsterService")
	CurrencyService = Knit.GetService("CurrencyService")
	
	local cardChoices = {}
	
	MonsterService.StartMonsterDeck:Connect(function(cards)
		table.clear(cardChoices)
		for _, v in pairs(cards) do
			table.insert(cardChoices, v)
		end
		coroutine.wrap(function()
			TimerUI:SetAttribute("Enabled", false)
			ChooseUI.Enabled = true
			FadeFrameChooseUI.Visible = true
			FadeFrameChooseUI.BackgroundTransparency = 1
			PlaySound("Fade")
			tweenService:Create(FadeFrameChooseUI, TweenInfo.new(1), {BackgroundTransparency = 0}):Play()
			task.wait(1)
			TimerUI:SetAttribute("Enabled", false)
			LightingEvent:Fire("ChooseMonster")
			enableCameraTilt(workspace.Map.Cinematics.CardSelector.Camera)
			cam.FieldOfView = 70
			PlaySound("Fade")
			tweenService:Create(FadeFrameChooseUI, TweenInfo.new(1), {BackgroundTransparency = 1}):Play()
			task.wait(1)
			FadeFrameChooseUI.Visible = false
		end)()
		
		task.wait(3)
		AppearCards(cards)
	end)
	
	MonsterService.EndMonsterDeck:Connect(function()
		ChooseUI.Enabled = false
		disableCameraTilt()
		TimerUI:SetAttribute("Enabled", true)
		if #workspace.Map.Game:GetChildren() > 0 then
			for _, v in pairs(workspace.Map.Game:GetChildren()) do
				if v:IsA("Folder") and v:FindFirstChild("Map") then
					LightingEvent:Fire(v.Name)
				end
			end
		end
		
		workspace.Map.Cinematics.CardSelector.Cards:ClearAllChildren()
		currentSelected = nil
		table.clear(cardChoices)
		task.spawn(CleanHoverCards)
	end)
	
	MonsterService.StartMonsterGameplay:Connect(function(card)
		Active = true
		task.spawn(InitCameraController, card)
	end)
	
	MonsterService.EndMonsterGameplay:Connect(function()
		Active = false
		task.spawn(EndCameraController)
	end)
	
	MonsterService.UpdateCountdown:Connect(function(currentTime)
		TimeText.Text = tostring(currentTime)
	end)
	
	MonsterService.Select:Connect(function(card)
		warn(`recieved, selected card is {card.Name}`)
		CurrentMonsterSelected = card
	end)
	
	UserInputService.InputBegan:Connect(function(input)
		if Active == true and player:GetAttribute("IsMonster") == true and game.Workspace:GetAttribute("InGame") == true and player.Character and player.Character.Humanoid and player.Character.Humanoid.Health > 0 then
			if input.KeyCode == Enum.KeyCode.Z or input.KeyCode == Enum.KeyCode.ButtonL1 then
				if player.CameraMode == Enum.CameraMode.LockFirstPerson then
					player.CameraMode = Enum.CameraMode.Classic

					local currentCFrame = cam.CFrame
					local lookVector = currentCFrame.LookVector
					local flatLookVector = Vector3.new(lookVector.X, 0, lookVector.Z).Unit
					cam.CFrame = CFrame.new(currentCFrame.Position + Vector3.new(0,0.35,0), currentCFrame.Position + flatLookVector)
				else
					player.CameraMode = Enum.CameraMode.LockFirstPerson
				end
			elseif input.KeyCode == Enum.UserInputType.MouseButton1 or input.KeyCode == Enum.KeyCode.ButtonR2 then
				-- attack
				if CurrentMonsterSelected then
					
				end
			end
		end
	end)
end

Then I made these functions

local currentSelected
local HoverCards = {}
local function setupCards()
	local cardHitboxes = {}
	for _, card in pairs(workspace.Map.Cinematics.CardSelector.Hitboxes:GetChildren()) do
		table.insert(cardHitboxes, card)
	end
	
	local hoverCards = MouseCastPlus.new(cardHitboxes)
	table.insert(HoverCards, hoverCards)
	
	hoverCards:onHoverEnter(function(card)
		local cardmodel = getCardModelFromHitbox(card)
		task.spawn(hoveringEffectsCards, cardmodel, true)
	end)

	hoverCards:onHoverLeave(function(card)
		local cardmodel = getCardModelFromHitbox(card)
		task.spawn(hoveringEffectsCards, cardmodel, false)
	end)
	
	hoverCards:onClick(function(card)
		local cardmodel = getCardModelFromHitbox(card)
		local cardName = cardmodel.Name
		currentSelected = cardName
		MonsterService.Select:Fire(cardName)
	end)
end

local function CleanHoverCards()
	for _, HoverCard in pairs(HoverCards) do
		HoverCard:Destroy()
	end
	table.clear(HoverCards)
end

What I provided works for me and my game, and this might maybe help you with yours. Goodluck!

1 Like

Hi!
I followed all your advice and looked at the example code you gave (Which was pretty helpful)
However, I am still encountering the same bug.

Code:

--//Services
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local TweenService = game:GetService("TweenService")
local CollectionService = game:GetService("CollectionService")

--//Modules
local Modules = ReplicatedStorage.Modules

local MouseCastPlus = require(Modules.UI.Components.MouseCast)
local Information = require(ReplicatedStorage.Shared.Information)

--//Tagged
local ThreeDHiglightUITagged = Information.ThreeDInteraction.ThreeDHiglightUI
local ThreeDColorUITagged = Information.ThreeDInteraction.ThreeDColorUI
local ThreeDDoorUITagged = Information.ThreeDInteraction.ThreeDDoorUI

--//Main 
local Knit = require(ReplicatedStorage.Packages.Knit)

--//Module
local ThreeDInteractionController = Knit.CreateController  {
	Name = "ThreeDInteractionController",
}

--//Start
function ThreeDInteractionController:KnitStart()
	
	for _, Object in ipairs(ThreeDHiglightUITagged) do
		task.spawn(HandleHighlight, Object)
	end

	for _, Object in ipairs(ThreeDColorUITagged) do
		task.spawn(HandleColor, Object)
	end

	for _, Object in ipairs(ThreeDDoorUITagged) do
		task.spawn(HandleDoors, Object)
	end
	
	CollectionService:GetInstanceAddedSignal("3DHighlightUI"):Connect(function(newObject)
		task.spawn(HandleHighlight, newObject)
	end)
	
	CollectionService:GetInstanceAddedSignal("3DColorUI"):Connect(function(newObject)
		task.spawn(HandleColor, newObject)
	end)
	
	CollectionService:GetInstanceAddedSignal("3DDoorUI"):Connect(function(newObject)
		task.spawn(HandleDoors, newObject)
	end)
	
	
end

--//Methods
function HandleHighlight(Object)
	local hoverObject = MouseCastPlus.new(Object)
	Object:SetAttribute("HoverActive", false)

	hoverObject:onHoverEnter(function(part)
		
		if not Object:GetAttribute("HoverActive") then
			Object:SetAttribute("HoverActive", true)
			
			local highlight = part:FindFirstChildOfClass("Highlight")
			if highlight then
				TweenService:Create(highlight, TweenInfo.new(0.25), {OutlineTransparency = 0}):Play()
			end
			
		end
		
	end)

	hoverObject:onHoverLeave(function(part)
		
		if Object:GetAttribute("HoverActive") then
			Object:SetAttribute("HoverActive", false)
			
			local highlight = part:FindFirstChildOfClass("Highlight")
			if highlight then
				TweenService:Create(highlight, TweenInfo.new(0.25), {OutlineTransparency = 1}):Play()
			end
			
		end
		

	end)

	hoverObject:onClick(function(part)
		if part:GetAttribute("Enabled") == nil then
			part:SetAttribute("Enabled", false)
		end

		local highlight = part:FindFirstChildOfClass("Highlight")

		if part:GetAttribute("Enabled") == true then
			part:SetAttribute("Enabled", false)

			if highlight then
				TweenService:Create(highlight, TweenInfo.new(0.25), {FillTransparency = 1}):Play()
			end
		else
			part:SetAttribute("Enabled", true)

			if highlight then
				TweenService:Create(highlight, TweenInfo.new(0.25), {FillTransparency = 0.5}):Play()
			end
		end
	end)
end

function HandleColor(Object)
	local hoverObject = MouseCastPlus.new(Object) -- create new parts
	Object:SetAttribute("HoverActive", false)

	hoverObject:onHoverEnter(function(part) -- mouse hovers over the object
		
		if not Object:GetAttribute("HoverActive") then
			Object:SetAttribute("HoverActive", true)
			
			local tween = TweenService:Create(part, TweenInfo.new(0.25), {Color = Color3.new(0.333333, 1, 0.498039)}) -- green
			tween:Play()
			
		end

	end)

	hoverObject:onHoverLeave(function(part) -- mouse leaves
		
		if Object:GetAttribute("HoverActive") then
			Object:SetAttribute("HoverActive", false)
			
			local tween = TweenService:Create(part, TweenInfo.new(0.25), {Color = Color3.new(1, 0, 0)}) -- red
			tween:Play()
			
		end

	end)

	hoverObject:onClick(function(part) -- mouse clicks
		local tween = TweenService:Create(part, TweenInfo.new(0.25), {Color = Color3.new(0.333333, 0.666667, 1)}) -- blue
		tween:Play()
	end)
end

function HandleDoors(Object)
	local hoverObject = MouseCastPlus.new(Object, 20)
	Object:SetAttribute("HoverActive", false)

	hoverObject:onHoverEnter(function(part)
		
		if not Object:GetAttribute("HoverActive") then
			Object:SetAttribute("HoverActive", true)
			
			local highlight = part.Parent:FindFirstChildOfClass("Highlight")
			if highlight then
				TweenService:Create(highlight, TweenInfo.new(0.25), {OutlineTransparency = 0}):Play()
			end
			
		end
		

	end)

	hoverObject:onHoverLeave(function(part)
		if Object:GetAttribute("HoverActive") then
			Object:SetAttribute("HoverActive", false)
			
			local highlight = part.Parent:FindFirstChildOfClass("Highlight")
			if highlight then
				TweenService:Create(highlight, TweenInfo.new(0.25), {OutlineTransparency = 1}):Play()
			end
		end

	end)

	hoverObject:onClick(function(door)
		if door.Parent:FindFirstChild("OpenEvent") then
			door.Parent.OpenEvent:FireServer()
		else
			warn("No remote found")
		end
	end)
end

return ThreeDInteractionController

Glad my examples helped! Maybe try using the :Disconnect or :Destroy functions in the updated version to clean it up a bit when it comes to changing objects?

Such as:

function HandleHighlight(Object)
    local hoverObject = MouseCastPlus.new(Object)
    local connectionTable = {} 

    Object:SetAttribute("HoverActive", false)

    local function connectEvents()
        connectionTable.hoverEnter = hoverObject:onHoverEnter(function(part)
            if not Object:GetAttribute("HoverActive") then
                Object:SetAttribute("HoverActive", true)
                local highlight = part:FindFirstChildOfClass("Highlight")
                if highlight then
                    TweenService:Create(highlight, TweenInfo.new(0.25), {OutlineTransparency = 0}):Play()
                end
            end
        end)

        connectionTable.hoverLeave = hoverObject:onHoverLeave(function(part)
            if Object:GetAttribute("HoverActive") then
                Object:SetAttribute("HoverActive", false)
                local highlight = part:FindFirstChildOfClass("Highlight")
                if highlight then
                    TweenService:Create(highlight, TweenInfo.new(0.25), {OutlineTransparency = 1}):Play()
                end
            end
        end)

        connectionTable.click = hoverObject:onClick(function(part)
            if part:GetAttribute("Enabled") == nil then
                part:SetAttribute("Enabled", false)
            end
            local highlight = part:FindFirstChildOfClass("Highlight")
            if part:GetAttribute("Enabled") == true then
                part:SetAttribute("Enabled", false)
                if highlight then
                    TweenService:Create(highlight, TweenInfo.new(0.25), {FillTransparency = 1}):Play()
                end
            else
                part:SetAttribute("Enabled", true)
                if highlight then
                    TweenService:Create(highlight, TweenInfo.new(0.25), {FillTransparency = 0.5}):Play()
                end
            end
        end)
    end

    local function disconnectEvents()
        for _, connection in pairs(connectionTable) do
            connection:Disconnect()
        end
        table.clear(connectionTable)
    end

    connectEvents()

    Object:GetAttributeChangedSignal("Reconnect"):Connect(function()
        if Object:GetAttribute("Reconnect") then
            disconnectEvents()
            connectEvents()
        end
    end)

    Object.Destroying:Connect(disconnectEvents)
end

Let me know if this helps :pray:

I am so sorry to keep bothering you :frowning: . The issue persists.

To give you a clearer image of what the issue is, here is an example video:
Watch the full video to understand!

External Media
Improved description:

Code will often run, often not. Leaving me confused as to what is causing this.

Maybe this is caused by an internal error in the module? Or maybe with how I modularized it, not sure. Will continue to try to fix it.

1 Like

oh so, basically sometimes when you load in, it doesnt work? Maybe you have to wait some time for the workspace to load in, or for those parts load in.

Edit: I didnt notice the drop down saying the code sometimes doesnt run, maybe add some more debugging lines to ensure its running?

I have done that, seems the issue persists anyways.
Maybe it is an issue with the for loop? or the module itself.

I have been trying to fix it but it simply, wont work.

1 Like

yeah, I will check it out in the module, I will try to update the module if I can find something that causes that in the module.

Ofcourse, In the meantime I will try to figure out if it is just a “Misuse” scenario.

Thank you for the help!

1 Like

Tried to get rid of the for loops, instead calling each object with a variable and calling the functions manually in KnitStart.

Seems to be much more consistent with the highlight and color parts, however the door object has completely lost functionality.

--//Services
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local TweenService = game:GetService("TweenService")
local CollectionService = game:GetService("CollectionService")

--//Modules
local Modules = ReplicatedStorage.Modules

local MouseCastPlus = require(Modules.UI.Components.MouseCast)
local Information = require(ReplicatedStorage.Shared.Information)

--//Tagged
local ThreeDHiglightUITagged = Information.ThreeDInteraction.ThreeDHiglightUI
local ThreeDColorUITagged = Information.ThreeDInteraction.ThreeDColorUI
local ThreeDDoorUITagged = Information.ThreeDInteraction.ThreeDDoorUI

--//Objects
local OtherFolder = workspace:WaitForChild("Map").Tangible.Other

local HighlightFolder = OtherFolder.Highlight
local ColorFolder = OtherFolder.Color
local DoorFolder = OtherFolder.SingleDoors

local HighlightPart1 = HighlightFolder:WaitForChild("Part1")
local HighlightPart2 = HighlightFolder:WaitForChild("Part2")

local ColorPart1 = ColorFolder:WaitForChild("Part1")
local ColorPart2 = ColorFolder:WaitForChild("Part2")

local Door = DoorFolder:WaitForChild("Door")

--//Main
local Knit = require(ReplicatedStorage.Packages.Knit)

--//Module
local ThreeDInteractionController = Knit.CreateController  {
	Name = "ThreeDInteractionController",
}

--//Start
function ThreeDInteractionController:KnitStart()
	
	HandleHighlight(HighlightPart1)
	HandleHighlight(HighlightPart2)
	
	HandleColor(ColorPart1)
	HandleColor(ColorPart2)
	
	HandleDoors(Door)
	
	CollectionService:GetInstanceAddedSignal("3DHighlightUI"):Connect(function(newObject)
		task.spawn(HandleHighlight, newObject)
	end)
	
	CollectionService:GetInstanceAddedSignal("3DColorUI"):Connect(function(newObject)
		task.spawn(HandleColor, newObject)
	end)
	
	CollectionService:GetInstanceAddedSignal("3DDoorUI"):Connect(function(newObject)
		task.spawn(HandleDoors, newObject)
	end)
	
	
end
1 Like

This leads me to believe it was caused by the for loop, but then I would have to question why the for loop even broke it to begin with, causing me to go back the functions it uses. And at last mouseCastPlus. Im looking into the module for any code that could break a for loop, or do something like this in the first place.

1 Like

Sorry if there are any issues, its my first module i released…

Dont worry! Its a solid module.
I fear the error may be in my code.