Hey everyone ![]()
I’m currently working on a melee combat system for my R6 PvP game and I’d like to add some swing VFX (like a slash effect) whenever the player attacks with their sword.
Right now, I already have:
- Animations set up (idle, walk, run, swing, block).
- Swing logic that plays the animation and handles cooldowns.
- My weapons and animations stored under ReplicatedStorage/Melee/[WeaponName].
- The actual VFX (a Part with the VFX attachment)
What I’m missing is the actual scripting logic and I’m unsure if positioning etc is correct.
Just want the swing VFX to appear when I press Q / left click to swing.
Here is my AnimHandler module script :
local AnimHandler = {}
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")
function AnimHandler.SetupAnimations(character: Model, weaponName: string, toolRef)
local humanoid = character:FindFirstChildOfClass("Humanoid")
if not humanoid then
warn("AnimHandler: No Humanoid")
return
end
local animateScript = character:FindFirstChild("Animate")
if animateScript then
animateScript.Disabled = true
end
local animator = humanoid:FindFirstChildOfClass("Animator")
if not animator then
animator = Instance.new("Animator")
animator.Name = "Animator"
animator.Parent = humanoid
animator:WaitForChild("AnimatorLoaded", 1)
end
local meleeFolder = ReplicatedStorage:WaitForChild("Melee")
local animFolder = meleeFolder:FindFirstChild(weaponName)
if not animFolder then
warn("AnimHandler: Missing weapon folder:", weaponName)
if animateScript then animateScript.Disabled = false end
return
end
local animSaves = animFolder:FindFirstChild("AnimSaves")
if not animSaves then
warn("AnimHandler: Missing AnimSaves folder for", weaponName)
if animateScript then animateScript.Disabled = false end
return
end
-- Fetch animation objects
local idleAnim = animSaves:FindFirstChild("Idle")
local walkAnim = animSaves:FindFirstChild("Walk")
local runAnim = animSaves:FindFirstChild("Run")
local swing1Anim = animSaves:FindFirstChild("Swing1")
local blockAnim = animSaves:FindFirstChild("Block")
if not swing1Anim then
warn("AnimHandler: Missing Swing1 animation for", weaponName)
if animateScript then animateScript.Disabled = false end
return
end
-- Load tracks
local idleTrack = idleAnim and animator:LoadAnimation(idleAnim)
local walkTrack = walkAnim and animator:LoadAnimation(walkAnim)
local runTrack = runAnim and animator:LoadAnimation(runAnim)
local swing1Track = animator:LoadAnimation(swing1Anim)
local blockTrack = blockAnim and animator:LoadAnimation(blockAnim)
-- Priorities
if idleTrack then idleTrack.Priority = Enum.AnimationPriority.Idle end
if walkTrack then walkTrack.Priority = Enum.AnimationPriority.Movement end
if runTrack then runTrack.Priority = Enum.AnimationPriority.Movement end
swing1Track.Priority = Enum.AnimationPriority.Action4
if blockTrack then blockTrack.Priority = Enum.AnimationPriority.Action end
-- Looping flags
if idleTrack then idleTrack.Looped = true end
if walkTrack then walkTrack.Looped = true end
if runTrack then runTrack.Looped = true end
if blockTrack then blockTrack.Looped = true end
-- --- State
local isSwinging = false
local isBlocking = false
local isRunning = false
local currentMove: "idle" | "walk" | "run" | nil = nil
local currentSwingTrack: AnimationTrack? = nil
local locomotionConn: RBXScriptConnection? = nil
local swingStoppedConn: RBXScriptConnection? = nil
-- --- Helpers
local function stopMovementTracks(fade: number?)
fade = fade or 0.12
if idleTrack and idleTrack.IsPlaying then idleTrack:Stop(fade) end
if walkTrack and walkTrack.IsPlaying then walkTrack:Stop(fade) end
if runTrack and runTrack.IsPlaying then runTrack:Stop(fade) end
currentMove = nil
end
local function playOnly(target: "idle" | "walk" | "run")
if currentMove == target then return end
if not idleTrack or not walkTrack or not runTrack then return end
local fadeTime = 0.15
if idleTrack.IsPlaying then idleTrack:Stop(fadeTime) end
if walkTrack.IsPlaying then walkTrack:Stop(fadeTime) end
if runTrack.IsPlaying then runTrack:Stop(fadeTime) end
task.wait(0.05)
if target == "idle" then
idleTrack:Play(fadeTime)
elseif target == "walk" then
walkTrack:Play(fadeTime)
elseif target == "run" then
runTrack:Play(fadeTime)
end
currentMove = target
end
local function refreshLocomotion()
if isSwinging or isBlocking then return end
if not (idleTrack and walkTrack and runTrack) then return end
local moving = humanoid.MoveDirection.Magnitude > 0.1
if not moving then
playOnly("idle")
else
playOnly(isRunning and "run" or "walk")
end
end
if idleTrack then
idleTrack:Play(0.15)
currentMove = "idle"
end
-- Locomotion driver
locomotionConn = RunService.Heartbeat:Connect(refreshLocomotion)
local api = {}
function api.Swing()
if isBlocking or isSwinging then return end
isSwinging = true
stopMovementTracks(0.06)
if swingStoppedConn then
swingStoppedConn:Disconnect()
swingStoppedConn = nil
end
if currentSwingTrack and currentSwingTrack.IsPlaying then
currentSwingTrack:Stop(0.03)
end
currentSwingTrack = swing1Track
currentSwingTrack.Looped = false
currentSwingTrack:Play(0.03)
swingStoppedConn = currentSwingTrack.Stopped:Connect(function()
isSwinging = false
if swingStoppedConn then
swingStoppedConn:Disconnect()
swingStoppedConn = nil
end
task.defer(refreshLocomotion)
end)
local fallbackLen = (currentSwingTrack.Length > 0 and currentSwingTrack.Length or 0.5) + 0.2
task.delay(fallbackLen, function()
if isSwinging then
isSwinging = false
if swingStoppedConn then
swingStoppedConn:Disconnect()
swingStoppedConn = nil
end
task.defer(refreshLocomotion)
end
end)
end
function api.SetRunning(state: boolean)
local wasRunning = isRunning
isRunning = state
humanoid.WalkSpeed = state and 24 or 16
if wasRunning ~= isRunning and (runTrack and walkTrack) then
if currentMove == "run" and not state then
runTrack:Stop(0.1)
currentMove = nil
elseif currentMove == "walk" and state then
walkTrack:Stop(0.1)
currentMove = nil
end
task.defer(refreshLocomotion)
end
end
function api.SetBlocking(state: boolean)
if state then
if isBlocking then return end
isBlocking = true
if isSwinging then
isSwinging = false
if swingStoppedConn then
swingStoppedConn:Disconnect()
swingStoppedConn = nil
end
if currentSwingTrack and currentSwingTrack.IsPlaying then
currentSwingTrack:Stop(0.03)
end
end
stopMovementTracks(0.06)
if blockTrack then
if not blockTrack.IsPlaying then blockTrack:Play(0.06) end
elseif idleTrack then
if not idleTrack.IsPlaying then idleTrack:Play(0.1) end
end
else
if not isBlocking then return end
isBlocking = false
if blockTrack and blockTrack.IsPlaying then
blockTrack:Stop(0.08)
end
task.defer(refreshLocomotion)
end
end
function api.Cleanup()
if locomotionConn then
locomotionConn:Disconnect()
locomotionConn = nil
end
if swingStoppedConn then
swingStoppedConn:Disconnect()
swingStoppedConn = nil
end
local allTracks = { idleTrack, walkTrack, runTrack, swing1Track, blockTrack }
for _, tr in ipairs(allTracks) do
if tr then
if tr.IsPlaying then tr:Stop(0.08) end
tr:Destroy()
end
end
if animateScript then
animateScript.Disabled = false
end
end
return api
end
return AnimHandler
Here is my Tool’s LocalScript :
--// Services
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local UserInputService = game:GetService("UserInputService")
local RunService = game:GetService("RunService")
local TweenService = game:GetService("TweenService")
local Debris = game:GetService("Debris")
local Workspace = game:GetService("Workspace")
--// Player / Character / Tool
local player = Players.LocalPlayer
local character = player.Character or player.CharacterAdded:Wait()
player.CharacterAdded:Connect(function(newChar) character = newChar end)
local tool = script.Parent
assert(tool and tool:IsA("Tool"), "This script must be a LocalScript under a Tool")
--// Remotes & Modules
local Remotes = ReplicatedStorage:WaitForChild("Remotes")
local meleeSwingEvent = Remotes:WaitForChild("MeleeSwing")
local blockStateEvent = Remotes:WaitForChild("BlockState")
local AnimHandler = require(ReplicatedStorage:WaitForChild("System"):WaitForChild("AnimHandler"))
--// Config
local CROSSHAIR_IMAGE = "rbxassetid://77159165581033"
local BASE_SIZE = 89
local EXPANDED_SIZE = 98
local EXPAND_TIME = 0.08
local CONTRACT_TIME = 0.12
local SWING_COOLDOWN = 1.0
local BAR_WIDTH = 20
local BAR_HEIGHT = 3
local BAR_MARGIN = 0
local BAR_EDGE_OFFSET = -20
local BAR_ANIMATION_TIME = 0.8
local HIDE_CURSOR_IN_STUDIO = true
--// Runtime refs
local swingFunc, setRunningFunc, setBlockingFunc, cleanupFunc
local isEquipped = false
local rsConn
local activeExpandTween, activeContractTween, activeLeftBarTween, activeRightBarTween
local lastSwingTime = 0
local playerGui = player:WaitForChild("PlayerGui")
local screenGui
local crosshair
local leftBar, rightBar
--======== Tween safety helpers ========
local function canTween(inst: Instance?)
return inst and inst.Parent ~= nil
end
local function safeTween(inst: Instance?, info: TweenInfo, props: table)
if not canTween(inst) then return nil end
local t
local ok, err = pcall(function()
t = TweenService:Create(inst, info, props)
end)
if not ok or not t then
warn("Tween creation failed:", err)
return nil
end
t:Play()
return t
end
local function cancelTween(t)
if t then
pcall(function()
t:Cancel()
t:Destroy()
end)
end
end
--======== UI build / destroy ========
local function destroyExistingUI()
local existingGui = playerGui:FindFirstChild("SwordCrosshairGui")
if existingGui then
existingGui:Destroy()
end
end
local function createUI()
destroyExistingUI()
screenGui = Instance.new("ScreenGui")
screenGui.Name = "SwordCrosshairGui"
screenGui.IgnoreGuiInset = true
screenGui.ResetOnSpawn = false
screenGui.ZIndexBehavior = Enum.ZIndexBehavior.Sibling
screenGui.Parent = playerGui
-- Crosshair
crosshair = Instance.new("ImageLabel")
crosshair.Name = "Crosshair"
crosshair.AnchorPoint = Vector2.new(0.5, 0.5)
crosshair.Size = UDim2.fromOffset(BASE_SIZE, BASE_SIZE)
crosshair.BackgroundTransparency = 1
crosshair.Image = CROSSHAIR_IMAGE
crosshair.ImageTransparency = 0
crosshair.ZIndex = 1000
crosshair.Visible = false
crosshair.Parent = screenGui
-- Left progress bar
leftBar = Instance.new("Frame")
leftBar.Name = "LeftCooldownBar"
leftBar.AnchorPoint = Vector2.new(1, 0.5)
leftBar.Size = UDim2.fromOffset(0, BAR_HEIGHT)
leftBar.BackgroundColor3 = Color3.fromRGB(255, 255, 255)
leftBar.BackgroundTransparency = 0.1
leftBar.BorderSizePixel = 0
leftBar.ZIndex = 1001
leftBar.Visible = false
leftBar.Parent = screenGui
-- Right progress bar
rightBar = Instance.new("Frame")
rightBar.Name = "RightCooldownBar"
rightBar.AnchorPoint = Vector2.new(0, 0.5)
rightBar.Size = UDim2.fromOffset(0, BAR_HEIGHT)
rightBar.BackgroundColor3 = Color3.fromRGB(255, 255, 255)
rightBar.BackgroundTransparency = 0.1
rightBar.BorderSizePixel = 0
rightBar.ZIndex = 1001
rightBar.Visible = false
rightBar.Parent = screenGui
end
local function destroyUI()
if rsConn then
rsConn:Disconnect()
rsConn = nil
end
cancelTween(activeExpandTween)
cancelTween(activeContractTween)
cancelTween(activeLeftBarTween)
cancelTween(activeRightBarTween)
activeExpandTween, activeContractTween, activeLeftBarTween, activeRightBarTween = nil, nil, nil, nil
if screenGui then
screenGui:Destroy()
end
screenGui, crosshair, leftBar, rightBar = nil, nil, nil, nil
end
local function setMouseVisible(visible)
if HIDE_CURSOR_IN_STUDIO or not RunService:IsStudio() then
UserInputService.MouseIconEnabled = visible
end
end
local lastMousePos = Vector2.new(0, 0)
local function startMouseFollow()
if rsConn then rsConn:Disconnect() end
rsConn = RunService.RenderStepped:Connect(function()
if not crosshair then return end
local pos = UserInputService:GetMouseLocation()
if (pos - lastMousePos).Magnitude < 1 then return end
lastMousePos = pos
crosshair.Position = UDim2.fromOffset(pos.X, pos.Y)
local currentSizeX = crosshair.AbsoluteSize.X
if currentSizeX <= 0 then currentSizeX = BASE_SIZE end
local radius = currentSizeX * 0.5
local edge = radius + BAR_MARGIN + BAR_EDGE_OFFSET
if leftBar then
leftBar.Position = UDim2.fromOffset(pos.X - edge, pos.Y)
end
if rightBar then
rightBar.Position = UDim2.fromOffset(pos.X + edge, pos.Y)
end
end)
end
local function playCrosshairPulse()
if not canTween(crosshair) then return end
cancelTween(activeExpandTween)
cancelTween(activeContractTween)
crosshair.Size = UDim2.fromOffset(BASE_SIZE, BASE_SIZE)
activeExpandTween = safeTween(
crosshair,
TweenInfo.new(EXPAND_TIME, Enum.EasingStyle.Back, Enum.EasingDirection.Out),
{ Size = UDim2.fromOffset(EXPANDED_SIZE, EXPANDED_SIZE) }
)
if activeExpandTween then
activeExpandTween.Completed:Once(function()
if not canTween(crosshair) then return end
activeContractTween = safeTween(
crosshair,
TweenInfo.new(CONTRACT_TIME, Enum.EasingStyle.Quad, Enum.EasingDirection.Out),
{ Size = UDim2.fromOffset(BASE_SIZE, BASE_SIZE) }
)
end)
end
end
-- Cooldown bars
local function playCooldownBars()
if not (canTween(leftBar) and canTween(rightBar)) then return end
leftBar.Visible = true
rightBar.Visible = true
leftBar.Size = UDim2.fromOffset(0, BAR_HEIGHT)
rightBar.Size = UDim2.fromOffset(0, BAR_HEIGHT)
leftBar.BackgroundTransparency = 0.1
rightBar.BackgroundTransparency = 0.1
cancelTween(activeLeftBarTween)
cancelTween(activeRightBarTween)
local barInfo = TweenInfo.new(BAR_ANIMATION_TIME, Enum.EasingStyle.Quad, Enum.EasingDirection.Out)
activeLeftBarTween = safeTween(leftBar, barInfo, { Size = UDim2.fromOffset(BAR_WIDTH, BAR_HEIGHT) })
activeRightBarTween = safeTween(rightBar, barInfo, { Size = UDim2.fromOffset(BAR_WIDTH, BAR_HEIGHT) })
local function onBarsFinished()
if not (leftBar and rightBar) then return end
local fadeInfo = TweenInfo.new(0.2, Enum.EasingStyle.Quad, Enum.EasingDirection.Out)
if canTween(leftBar) then
safeTween(leftBar, fadeInfo, { BackgroundTransparency = 1 })
end
if canTween(rightBar) then
safeTween(rightBar, fadeInfo, { BackgroundTransparency = 1 })
end
task.wait(0.25)
if leftBar then
leftBar.Visible = false
leftBar.BackgroundTransparency = 0.1
end
if rightBar then
rightBar.Visible = false
rightBar.BackgroundTransparency = 0.1
end
end
if activeLeftBarTween then activeLeftBarTween.Completed:Once(onBarsFinished) end
end
local function showCrosshairUI()
createUI()
if crosshair then crosshair.Visible = true end
setMouseVisible(false)
startMouseFollow()
end
local function hideCrosshairUI()
setMouseVisible(true)
destroyUI()
end
local function canSwing()
return (tick() - lastSwingTime) >= SWING_COOLDOWN
end
-- Equip / Unequip
tool.Equipped:Connect(function()
isEquipped = true
task.wait()
local handlers = AnimHandler.SetupAnimations(character, tool.Name or "WoodSword", tool)
if handlers then
swingFunc = handlers.Swing
setRunningFunc = handlers.SetRunning
setBlockingFunc = handlers.SetBlocking
cleanupFunc = handlers.Cleanup
end
showCrosshairUI()
end)
tool.Unequipped:Connect(function()
isEquipped = false
if cleanupFunc then cleanupFunc() end
swingFunc, setRunningFunc, setBlockingFunc, cleanupFunc = nil, nil, nil, nil
blockStateEvent:FireServer(false)
hideCrosshairUI()
end)
tool.Activated:Connect(function() end)
-- INPUT HANDLING
local inputConnections = {}
local function doSwing()
if not canSwing() then return end
if swingFunc then
lastSwingTime = tick()
swingFunc()
meleeSwingEvent:FireServer()
playCrosshairPulse()
playCooldownBars()
end
end
local function startBlock()
if setBlockingFunc then setBlockingFunc(true) end
blockStateEvent:FireServer(true)
end
local function stopBlock()
if setBlockingFunc then setBlockingFunc(false) end
blockStateEvent:FireServer(false)
end
local function connectInput()
-- Clean old
for _, conn in pairs(inputConnections) do
conn:Disconnect()
end
table.clear(inputConnections)
inputConnections.began = UserInputService.InputBegan:Connect(function(input, gp)
if gp or not isEquipped then return end
-- Run (hold)
if input.KeyCode == Enum.KeyCode.LeftShift then
if setRunningFunc then setRunningFunc(true) end
return
end
-- Swing: Q OR Left Click
if input.KeyCode == Enum.KeyCode.Q
or input.UserInputType == Enum.UserInputType.MouseButton1 then
doSwing()
return
end
-- Block (hold): F OR Right Click
if input.KeyCode == Enum.KeyCode.F
or input.UserInputType == Enum.UserInputType.MouseButton2 then
startBlock()
return
end
end)
inputConnections.ended = UserInputService.InputEnded:Connect(function(input, gp)
if gp or not isEquipped then return end
-- Stop run
if input.KeyCode == Enum.KeyCode.LeftShift then
if setRunningFunc then setRunningFunc(false) end
return
end
-- Stop block: F release OR Right Click release
if input.KeyCode == Enum.KeyCode.F
or input.UserInputType == Enum.UserInputType.MouseButton2 then
stopBlock()
return
end
end)
end
-- Initialize input connections
connectInput()
-- Clean up on tool removal
tool.AncestryChanged:Connect(function()
if not tool.Parent then
for _, conn in pairs(inputConnections) do
conn:Disconnect()
end
destroyUI()
end
end)
Finally, here is my directory :
If anyone can help me, I’d really appreciate it ![]()
Thanks in advance!
