While working on my bunnyhop module, i decided to get it from testing idea to an actual module, after finishing it up, every value is correct, BUT the constructor seems to be called twice by the same line? I at first thought ‘oh, its being requested and used by another script’, i went to an empty baseplate and turned the module into a local script, and the issue persists, images of relevance:
I think this is happening because of setmetatable and the return bunnyhop.new()(assuming the return line is inside the bunnyhop module). Try returning bunnyhop instead and create the instance on another script with bunnyhop.new().
When you return an invocation of the constructor inside of the module you are using the singleton pattern. Somewhere else in your code you are requiring the module and calling the constructor again.
Sometimes when joining the game, the character “spawns twice” and has the code run twice.
If you are using a Normal Script with a Different RunContext, the script will both run in the Container and In the Character, as far as I know this only happens with “Starter” containers.
Are you using a regular local script or a script with the RunContext client? If the script is where the RunContext=Client, then you should add a check to the beginning of the script, for example this
if script.Parent.Name~=game.Players.LocalPlayer.Name then return end
(if script parent name ~= character (player) name)
or
if script.Parent.Name=="StarterCharacterScripts" then return end
(if script is in folder)
OR
Just make the localscript (Not script with RunContext)
i did think about this, but i looked at the code very carefully and the most relevant references that are even remotely related to the constructor was just me asserting a value to the object variables, after decoupling the constructor call into a separate script, still unfortunately the same issue, and i’m starting to doubt my sanity so, if you want to take a look at the whole thing here it is:
local bunnyhop = {}
bunnyhop.__index = bunnyhop
--[[SETTINGS]]
local LOOKVECTOR_UPDATE_TICK = 0.1 -- How fast to update the direction of the velocity ( avoiding turn delay )
local MINIMUM_VELOCITY = 0 -- The default velocity of the bhop, recommend to keep at 0
local MAXIMUM_VELOCITY = 300-- Self explanatory
local VELOCITY_MULTIPLIER = 2.5 -- by what increment to increase velocity
local VELOCITY_DEGRADATION = 0.5 -- how much to decrease velocity per tick
local VELOCITY_DEGRADATION_TICK = 0.1 -- how frequent to degrade velocity relative to above constant
local CUSTOM_BODY_VELOCITY = nil -- by default the script creates a body velocity,if you want a specific one put the path here
local STANDING_STILL_SAFEGUARD = true -- bhop wont fire if the player is standing still (true | false)
local VELOCITY_VALIDATOR_TICK = 0.7 -- The time frame a user is allowed to continue a bunnyhop
local velocityIndex = 0 -- Holds velocity multiplication data (VITAL)
local jumpValueHolder = false
--[[MAIN]] --------------------------------------------------
local UserInputService = game:GetService('UserInputService')
-- [[Client dependencies]]
local Player = game.Players.LocalPlayer;
local Character = Player.Character or Player.CharacterAdded:Wait();
local Humanoid = Character:WaitForChild("Humanoid");
local HumanoidRootPart = Character:WaitForChild("HumanoidRootPart");
function bunnyhop.new()
warn(debug.traceback("constructor caller:"))
local self = setmetatable({}, bunnyhop)
self._JumpRequestConnection = nil
self._LandedRequestConnection = nil
self._loopBreakCondition = true
self._velocityObject = nil
self:Enable()
return self
end
function bunnyhop:Enable()
print(debug.traceback("caller:"))
local VelocityObj
if CUSTOM_BODY_VELOCITY == nil then
VelocityObj = self._createVelocityObject()
else
assert(typeof(CUSTOM_BODY_VELOCITY) == "BodyVelocity", "Custom body velocity of invalid type, expected type 'BodyVelocity' ")
VelocityObj = CUSTOM_BODY_VELOCITY
end
self._loopBreakCondition = true
self._velocityObject = VelocityObj
self._JumpRequestConnection = UserInputService.JumpRequest:Connect(function()
if STANDING_STILL_SAFEGUARD then
if Humanoid.MoveDirection == Vector3.new() then
velocityIndex = MINIMUM_VELOCITY
return
end
end
print("Bunnyhopped")
velocityIndex += VELOCITY_MULTIPLIER / 3
jumpValueHolder = true
end)
self._LandedRequestConnection = Humanoid.StateChanged:Connect(function(New, Old)
if New == Enum.HumanoidStateType.Landed then
jumpValueHolder = false
print("Landed?")
task.delay(VELOCITY_VALIDATOR_TICK, function()
if not jumpValueHolder then
velocityIndex = MINIMUM_VELOCITY
end
end)
end
end)
self:_SpawnThreads()
end
function bunnyhop:_SpawnThreads()
task.spawn(function()
print("Thread #1")
while task.wait(VELOCITY_DEGRADATION_TICK) do
if not self._loopBreakCondition then break end
if velocityIndex <= MINIMUM_VELOCITY then continue end
velocityIndex -= VELOCITY_DEGRADATION
end
end)
task.spawn(function()
print("Thread #2")
while task.wait(LOOKVECTOR_UPDATE_TICK) do
self._velocityObject.Velocity = HumanoidRootPart.CFrame.LookVector * velocityIndex
end
end)
end
function bunnyhop:Disable()
self:_terminateVelocityObject()
self._loopBreakCondition = false
self._JumpRequestConnection:Disconnect()
self._LandedRequestConnection:Disconnect()
end
function bunnyhop:_terminateVelocityObject()
if self._velocityObject then
self._velocityObject:Destroy()
self._velocityObject = nil
end
end
--[[Static methods]]-------------------------------------------
function bunnyhop._createVelocityObject()
local BodyVelocity = Instance.new("BodyVelocity", HumanoidRootPart)
BodyVelocity.MaxForce = Vector3.new(MAXIMUM_VELOCITY, 0, MAXIMUM_VELOCITY)
return BodyVelocity
end
--[[Auto start]] ----------------------------------------------
print("IS THIS BEING CALLED TWICE OMG")
return bunnyhop
oh and, im a big fan of your posts samjay, your guides are great
( im aware that im kind of violating SRP, but since the system is pretty small it would have made things harder )
It appears that the reason your code is producing unexpected behavior is due to the functionality of StarterCharacterScripts. Upon character spawning, it duplicates all content within its designated folder, including any module scripts. Consequently, if a module is “loaded” during the character spawn, it will generate another module that requires loading every time the player respawns leading to the issue you are experiencing. Moving the module to RepStorage should resolve this issue, allowing for the use of a singleton per client by returning the constructor.
Your appreciation is greatly appreciated, and it motivates me to contribute further. When I first started my series on clean code I didn’t really expect much, but it really means a lot that I am truly impacting developers on the platform. It makes it all worth it.
Thank you, and on an unrelated note, i noticed that the velocity from the bhop isn’t applying, even though checking the value shows the right thing, no velocity is actually being applied for some reason?