[HELP] Base class running twice and properties aren't shared

Hi,

I have a base class that should store states. For example, when the player is sprinting, isSprinting becomes true. Problem is when I was debugging the base class, base class was printing twice instead of once. Another problem is that the properties are not shared.
I changed the title from “Module printing twice instead of once” to “Base class running twice and properties aren’t shared.” to make it more clear.

Here’s how I was debugging:

function BaseMovement.new(): ClassType
	local self = {
		character = nil,
		-- states?
		isWalking = false,
		isSprinting = false,
		isWallrunning = false,
		isFloating = false,
		isSliding = false,

		BaseConnections = Trove.new()
	}

	setmetatable(self, BaseMovement)

    -- debugging
	task.spawn(function()
		while task.wait(3) do
			print(self.isSprinting)
		end
	end)

	self:_init()

	return self
end

This printed out false 2x. And when the player was sprinting, it printed out true 1x and false 1x.

This is because I currently have two sub-classes that are accessing BaseMovement.
Screen Shot 2024-04-28 at 12.00.02 AM
Float and sprint are accessing BaseMovement. Sprint is changing isSprinting to true, but float return isSprinting to false.

Sprint
--!strict
--[[
	Sprint sub-class
--]]

--// roblox services
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local TweenService = game:GetService("TweenService")
--// variables
local CurrentCamera = workspace.CurrentCamera
local CameraTweenIn = TweenService:Create(CurrentCamera, TweenInfo.new(0.2), {FieldOfView = 75})
local CameraTweenOut = TweenService:Create(CurrentCamera, TweenInfo.new(0.2), {FieldOfView = 70})
--// Dependencies
local PlayerSettings = require(ReplicatedStorage.PlayerSettings)
local BaseMovement = require(script.Parent.BaseMovement)
local Trove = require(ReplicatedStorage.Packages._Index["sleitnick_trove@1.1.0"]["trove"])
--// class
local Sprint = setmetatable({}, {__index = BaseMovement})
Sprint.__index = Sprint

export type ClassType = typeof( setmetatable({} ::  {
	_connections: Trove.ClassType;
	--Bind: (self: ClassType, actionName: string, inputState: Enum.UserInputState, _inputObject: any) -> ();
} , Sprint) ) & BaseMovement.ClassType

--// constructor
function Sprint.new(): ClassType
	local self = setmetatable(BaseMovement.new() :: any, Sprint)
	
	self._connections = Trove.new()
	
	return self
end

function Sprint.Bind(self: ClassType, actionName, inputState, _inputObject): ()
	if not self.character then
		return
	end
	
	local Character = self.character
	local Humanoid = Character:FindFirstChild("Humanoid") :: Humanoid
	
	if inputState == Enum.UserInputState.Begin then
		self.isSprinting = true
		CameraTweenIn:Play()
		Humanoid.WalkSpeed = PlayerSettings.CharacterSprintSpeed
	end

	if inputState == Enum.UserInputState.End then
		CameraTweenOut:Play()
		Humanoid.WalkSpeed = PlayerSettings.CharacterWalkSpeed
		self.isSprinting = false
	end

end

function Sprint.GetParameters()
	return {
		actionName = "Sprint", -- mandatory
		createTouchButton = false, -- optional, will always be false on default
		inputs = { -- mandatory
			Enum.KeyCode.LeftShift,
			Enum.KeyCode.RightShift
		}
	}
end

return Sprint.new()
Float
--!strict
--[[
	Float sub-class
--]]

--// roblox services
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local TweenService = game:GetService("TweenService")
--// Dependencies
local BaseMovement = require(script.Parent.BaseMovement)
--// class
local Float = setmetatable({}, {__index = BaseMovement})
Float.__index = Float

export type ClassType = typeof( setmetatable({} ::  {

} , Float) ) & BaseMovement.ClassType

--// constructor
function Float.new(): ClassType
	local self = setmetatable(BaseMovement.new() :: any, Float)
	
	return self
end

function Float.Bind(self: ClassType, actionName, inputState, _inputObject): ()
	if not self.character then
		return
	end
	
	local Character = self.character
	local Humanoid = Character:FindFirstChild("Humanoid") :: Humanoid
	
	if inputState == Enum.UserInputState.Begin then
		print("Float start")
		self.isFloating = true
	end

	if inputState == Enum.UserInputState.End then
		print("Float end")
		self.isFloating = false
	end

end

function Float.GetParameters()
	return {
		actionName = "Float", -- mandatory
		createTouchButton = false, -- optional, will always be false on default
		inputs = { -- mandatory
			Enum.KeyCode.F,
		}
	}
end

return Float.new()

REPRO PLACE FILE ( REMOVE THE JUMP SUBCLASS):
baseclass_repro.rbxl (187.0 KB)

An example of this working is in roblox’s control subclasses.

Robloxs Base Class:

--[[
	BaseCharacterController - Abstract base class for character controllers, not intended to be
	directly instantiated.

	2018 PlayerScripts Update - AllYourBlox
--]]

local ZERO_VECTOR3: Vector3 = Vector3.new(0,0,0)

--[[ The Module ]]--
local BaseCharacterController = {}
BaseCharacterController.__index = BaseCharacterController

function BaseCharacterController.new()
	local self = setmetatable({}, BaseCharacterController)
	self.enabled = false
	self.moveVector = ZERO_VECTOR3
	self.moveVectorIsCameraRelative = true
	self.isJumping = false
	return self
end

function BaseCharacterController:OnRenderStepped(dt: number)
	-- By default, nothing to do
end

function BaseCharacterController:GetMoveVector(): Vector3
	return self.moveVector
end

function BaseCharacterController:IsMoveVectorCameraRelative(): boolean
	return self.moveVectorIsCameraRelative
end

function BaseCharacterController:GetIsJumping(): boolean
	return self.isJumping
end

-- Override in derived classes to set self.enabled and return boolean indicating
-- whether Enable/Disable was successful. Return true if controller is already in the requested state.
function BaseCharacterController:Enable(enable: boolean): boolean
	error("BaseCharacterController:Enable must be overridden in derived classes and should not be called.")
	return false
end

return BaseCharacterController

And for some reason BaseCharacterController is able to print once instead of the amount of subclasses that are using BaseCharacterController.

Roblox's Subclass - Keyboard (which uses its basecharactercontroller)
--!nonstrict
--[[
	Keyboard Character Control - This module handles controlling your avatar from a keyboard

	2018 PlayerScripts Update - AllYourBlox
--]]

--[[ Roblox Services ]]--
local UserInputService = game:GetService("UserInputService")
local ContextActionService = game:GetService("ContextActionService")

--[[ Constants ]]--
local ZERO_VECTOR3 = Vector3.new(0,0,0)

--[[ The Module ]]--
local BaseCharacterController = require(script.Parent:WaitForChild("BaseCharacterController"))
local Keyboard = setmetatable({}, BaseCharacterController)
Keyboard.__index = Keyboard

function Keyboard.new(CONTROL_ACTION_PRIORITY)
	local self = setmetatable(BaseCharacterController.new() :: any, Keyboard)

	self.CONTROL_ACTION_PRIORITY = CONTROL_ACTION_PRIORITY

	self.textFocusReleasedConn = nil
	self.textFocusGainedConn = nil
	self.windowFocusReleasedConn = nil
	
	self.forwardValue  = 0
	self.backwardValue = 0
	self.leftValue = 0
	self.rightValue = 0

	self.jumpEnabled = true

	return self
end

function Keyboard:Enable(enable: boolean)
	if not UserInputService.KeyboardEnabled then
		return false
	end
	if enable == self.enabled then
		-- Module is already in the state being requested. True is returned here since the module will be in the state
		-- expected by the code that follows the Enable() call. This makes more sense than returning false to indicate
		-- no action was necessary. False indicates failure to be in requested/expected state.
		return true
	end

	self.forwardValue  = 0
	self.backwardValue = 0
	self.leftValue = 0
	self.rightValue = 0
	self.moveVector = ZERO_VECTOR3
	self.jumpRequested = false
	self:UpdateJump()

	if enable then
		self:BindContextActions()
		self:ConnectFocusEventListeners()
	else
		self:UnbindContextActions()
		self:DisconnectFocusEventListeners()
	end

	self.enabled = enable
	return true
end

function Keyboard:UpdateMovement(inputState)
	if inputState == Enum.UserInputState.Cancel then
		self.moveVector = ZERO_VECTOR3
	else
		self.moveVector = Vector3.new(self.leftValue + self.rightValue, 0, self.forwardValue + self.backwardValue)
	end
end

function Keyboard:UpdateJump()
	self.isJumping = self.jumpRequested
end

function Keyboard:BindContextActions()

	-- Note: In the previous version of this code, the movement values were not zeroed-out on UserInputState. Cancel, now they are,
	-- which fixes them from getting stuck on.
	-- We return ContextActionResult.Pass here for legacy reasons.
	-- Many games rely on gameProcessedEvent being false on UserInputService.InputBegan for these control actions.
	local handleMoveForward = function(actionName, inputState, inputObject)
		self.forwardValue = (inputState == Enum.UserInputState.Begin) and -1 or 0
		self:UpdateMovement(inputState)
		return Enum.ContextActionResult.Pass
	end

	local handleMoveBackward = function(actionName, inputState, inputObject)
		self.backwardValue = (inputState == Enum.UserInputState.Begin) and 1 or 0
		self:UpdateMovement(inputState)
		return Enum.ContextActionResult.Pass
	end

	local handleMoveLeft = function(actionName, inputState, inputObject)
		self.leftValue = (inputState == Enum.UserInputState.Begin) and -1 or 0
		self:UpdateMovement(inputState)
		return Enum.ContextActionResult.Pass
	end

	local handleMoveRight = function(actionName, inputState, inputObject)
		self.rightValue = (inputState == Enum.UserInputState.Begin) and 1 or 0
		self:UpdateMovement(inputState)
		return Enum.ContextActionResult.Pass
	end

	local handleJumpAction = function(actionName, inputState, inputObject)
		self.jumpRequested = self.jumpEnabled and (inputState == Enum.UserInputState.Begin)
		self:UpdateJump()
		return Enum.ContextActionResult.Pass
	end

	-- TODO: Revert to KeyCode bindings so that in the future the abstraction layer from actual keys to
	-- movement direction is done in Lua
	ContextActionService:BindActionAtPriority("moveForwardAction", handleMoveForward, false,
		self.CONTROL_ACTION_PRIORITY, Enum.PlayerActions.CharacterForward)
	ContextActionService:BindActionAtPriority("moveBackwardAction", handleMoveBackward, false,
		self.CONTROL_ACTION_PRIORITY, Enum.PlayerActions.CharacterBackward)
	ContextActionService:BindActionAtPriority("moveLeftAction", handleMoveLeft, false,
		self.CONTROL_ACTION_PRIORITY, Enum.PlayerActions.CharacterLeft)
	ContextActionService:BindActionAtPriority("moveRightAction", handleMoveRight, false,
		self.CONTROL_ACTION_PRIORITY, Enum.PlayerActions.CharacterRight)
	ContextActionService:BindActionAtPriority("jumpAction", handleJumpAction, false,
		self.CONTROL_ACTION_PRIORITY, Enum.PlayerActions.CharacterJump)
end

function Keyboard:UnbindContextActions()
	ContextActionService:UnbindAction("moveForwardAction")
	ContextActionService:UnbindAction("moveBackwardAction")
	ContextActionService:UnbindAction("moveLeftAction")
	ContextActionService:UnbindAction("moveRightAction")
	ContextActionService:UnbindAction("jumpAction")
end

function Keyboard:ConnectFocusEventListeners()
	local function onFocusReleased()
		self.moveVector = ZERO_VECTOR3
		self.forwardValue  = 0
		self.backwardValue = 0
		self.leftValue = 0
		self.rightValue = 0
		self.jumpRequested = false
		self:UpdateJump()
	end

	local function onTextFocusGained(textboxFocused)
		self.jumpRequested = false
		self:UpdateJump()
	end

	self.textFocusReleasedConn = UserInputService.TextBoxFocusReleased:Connect(onFocusReleased)
	self.textFocusGainedConn = UserInputService.TextBoxFocused:Connect(onTextFocusGained)
	self.windowFocusReleasedConn = UserInputService.WindowFocused:Connect(onFocusReleased)
end

function Keyboard:DisconnectFocusEventListeners()
	if self.textFocusReleasedConn then
		self.textFocusReleasedConn:Disconnect()
		self.textFocusReleasedConn = nil
	end
	if self.textFocusGainedConn then
		self.textFocusGainedConn:Disconnect()
		self.textFocusGainedConn = nil
	end
	if self.windowFocusReleasedConn then
		self.windowFocusReleasedConn:Disconnect()
		self.windowFocusReleasedConn = nil
	end
end

return Keyboard

How do I make it print once and make the properties shared?

2 Likes

O hai it’s you again lol but here is some possible things to consider here

The issue you’re encountering with your BaseMovement class printing the isSprinting state twice instead of once likely stems from multiple instances of the BaseMovement class being created, or less commonly, the debugging task being initiated more than once within the same instance. Here are a few steps and considerations to resolve or debug the issue:

function BaseMovement.new()
    local self = {
        character = nil,
        isWalking = false,
        isSprinting = false,
        isWallrunning = false,
        isFloating = false,
        isSliding = false,
        BaseConnections = Trove.new(),
        instanceId = math.random(), -- Unique identifier
    }

    setmetatable(self, BaseMovement)

    -- debugging with unique identifier
    task.spawn(function()
        while task.wait(3) do
            print("Instance:", self.instanceId, "isSprinting:", self.isSprinting)
        end
    end)

    self:_init()

    return self
end

We’ll start small

3 Likes

Hey lol, I have a tendency to ask for a lot of help.

Here’s my output, my first print was me running and the rest was not me running.

  00:22:16.012  Instance: 0.15894229992704315 isSprinting: false  -  Client - BaseMovement:45
  00:22:16.012  Instance: 0.07471862962891239 isSprinting: true  -  Client - BaseMovement:45
  00:22:19.027  Instance: 0.15894229992704315 isSprinting: false  -  Client - BaseMovement:45
  00:22:19.027  Instance: 0.07471862962891239 isSprinting: false  -  Client - BaseMovement:45
  00:22:22.043  Instance: 0.15894229992704315 isSprinting: false  -  Client - BaseMovement:45
  00:22:22.043  Instance: 0.07471862962891239 isSprinting: false  -  Client - BaseMovement:45
  00:22:23.262
2 Likes

Ok we will see here first with those print statements

It seems like the issue you’re encountering is related to the debugging mechanism inside your BaseMovement constructor function, specifically with the loop that continuously prints the sprinting status of each movement instance every 3 seconds. Your observation indicates that the sprinting state changes as expected, but you’re concerned about the way the task.wait(3) loop functions, especially in regard to how it might affect performance or not accurately reflect changes in state over time.

task.spawn(function()
    while self.debuggingEnabled and task.wait(3) do
        print("Instance:", self.instanceId, "isSprinting:", self.isSprinting)
    end
end)

Then, make sure to set self.debuggingEnabled = true in your constructor and provide a method to disable it when you’re finished debugging or when the object is destroyed.

Resource Cleanup

To ensure that your debugging tasks and any other resources are properly cleaned up, especially when instances of BaseMovement are no longer needed, you should implement a destructor or cleanup method. This method would set self.debuggingEnabled to false, disconnect any event connections stored in BaseConnections, and perform any other necessary cleanup operations to avoid memory leaks and ensure garbage collection can occur efficiently.

Example Cleanup Method

Add this method to your BaseMovement class:

function BaseMovement:Destroy()
    self.debuggingEnabled = false -- Stop debugging loop
    self.BaseConnections:Destroy() -- Assuming Trove is a utility to manage connections or resources
    -- Add any additional cleanup code here
end
4 Likes

Sounds good.

--!strict

--[[
    The base class for 'movement' // superclass
--]]

--// roblox services
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
--// Dependencies
local Trove = require(ReplicatedStorage.Packages._Index["sleitnick_trove@1.1.0"]["trove"])
--// class
local BaseMovement = {}
BaseMovement.__index = BaseMovement

export type ClassType = typeof(setmetatable({} :: {
	character: Model?;
	BaseConnections: Trove.ClassType;

	isWalking: boolean;
	isSprinting: boolean;
	isWallrunning: boolean;
	isFloating: boolean;
	isSliding: boolean;
	debuggingEnabled: boolean;

}, BaseMovement))

function BaseMovement.new() : ClassType
	local self = {
		character = nil,
		isWalking = false,
		isSprinting = false,
		isWallrunning = false,
		isFloating = false,
		isSliding = false,
		debuggingEnabled = true,
		
		BaseConnections = Trove.new(),
		instanceId = math.random(), -- Unique identifier
	}

	setmetatable(self, BaseMovement)

	-- debugging with unique identifier
	task.spawn(function()
		while self.debuggingEnabled and task.wait(3) do
			print("Instance:", self.instanceId, "isSprinting:", self.isSprinting)
		end
	end)

	self:_init()

	return self
end

function BaseMovement._init(self: ClassType): ()
	self.BaseConnections:Connect(Players.LocalPlayer.CharacterAdded, function(character: Model)
		if not self.character then
			self.character = character
		end
	end)

	if Players.LocalPlayer.Character then
		self.character = Players.LocalPlayer.Character
	end
end

function BaseMovement.GetCharacter(self: ClassType): Model?
	return self.character
end

function BaseMovement.Destroy(self: ClassType): ()
	self.debuggingEnabled = false 
	self.BaseConnections:Destroy()
end

return BaseMovement

I think that’s because float and sprint creates a class for itself, so if you change isSprinting in Sprint module, Float module won’t get it because Sprint module created a class for itself and changes it only for itself. It’s sad that there is no good OOP in Luau like in Java or C++

3 Likes

Yeah but what’s the difference in Roblox’s version? I basically did the same thing as them and it yielded different results.

The only real difference is I’m using strict mode while Roblox is using no strict mode

3 Likes

As i can explain again, your main module created two threads, Sprint and Float. This threads created another two threads of BaseMovement module. And because of that the line print(self.isSprinting) completes twice, but Sprint has different values of isSprinting than Float.
I don’t really know why you didn’t closed this post from now on.

3 Likes

Roblox has 8 classes (i checked) that inherit the Base Class and it doesn’t print out 8x instantaneously if you try to debug it like in my base class.

1 Like

Inheriting from a base class (like the super cool car) allows you to make new things (like different types of cars) with the same cool features without starting from scratch.

1 Like

update your basemovement function —

local self = {
    character = nil,
    isWalking = false,
    isSprinting = false,  -- Shared property
    isWallrunning = false,
    isFloating = false,  -- Shared property
    isSliding = false,
    BaseConnections = Trove.new()
}
2 Likes

also update for the sprint code -----

function Sprint.Bind(self: ClassType, actionName, inputState, _inputObject): ()
    if not self.character then
        return
    end
    
    -- ... rest of your code
    
    if inputState == Enum.UserInputState.Begin then
        self.isSprinting = true  -- Access shared property
        CameraTweenIn:Play()
        Humanoid.WalkSpeed = PlayerSettings.CharacterSprintSpeed
    end

    if inputState == Enum.UserInputState.End then
        CameraTweenOut:Play()
        Humanoid.WalkSpeed = PlayerSettings.CharacterWalkSpeed
        self.isSprinting = false -- Access shared property
    end
end
2 Likes

I already have that for float and sprint. It’s not shared like Roblox because basemovement prints more than once

1 Like

Still having issues with this… HELP WANTED

1 Like

more context is needed for this

2 Likes

I’m not sure what else to give you but I can send you a place file later

1 Like

Hey I sent you a friend request wanna collab on random games together for fun

1 Like

Uhh maybe after this issue gets fixed

1 Like

Alright let’s get it over with.

First what is the script that is causing the issue between a module or something

Where is the base class

1 Like

I’ll give you the place file tomorrow, I’m tired :sleeping_bed:

2 Likes