Floating Fan Platforms

Weirdly enough, even with streaming enabled parts don’t render out of my view when in-game, unlike in Studio, where parts remain parented to the models even when not rendered.

Buuuut, I noticed there are some unnecessary lines and things that can potentially cause errors, like looping through all anchor parts only to find the ones that can move, or checking if the float animation is playing while it hasn’t been loaded yet.

Anyways, I moved stuff around and updated the model. Here’s the new script compared to the old one:

New
--// Constants
local DEBUG = false -- Determines whether or not the float areas will be visible for the player.
local FLOAT_ACCELERATION = 100 -- How fast the player will accelerate upwards regardless of mass.
local INITIAL_FALLING_VELOCITY = -30 -- Sets player's vertical velocity to this value when they enter a float area while falling.
local ROTATIONS_PER_SECOND = 3 -- Spin frequency of the model's fan rotor parts.



--// Services
local COLLECTION_SERVICE = game:GetService("CollectionService")
local PLAYERS = game:GetService("Players")
local RUN_SERVICE = game:GetService("RunService")



--// Variables

-- Fan models
local initial_time = 0

local moving_anchor_parts = {}
local rotors = {}
local float_areas = {}

-- Floating
local character: Model
local humanoid: Humanoid
local root_part: BasePart

local dive_animation: AnimationTrack
local float_animation: AnimationTrack

local vector_force: VectorForce = script.VectorForce

local current_float_area: BasePart



--// Fan model setup
local function CloneSound(sound: Sound, parent: BasePart)
	sound = sound:Clone()
	sound.TimePosition = math.random(10, 100) / 10
	sound.Playing = true
	sound.Parent = parent
end

local function SetNewFan(fan: Model)
	local float_area: BasePart = fan:WaitForChild("FloatArea")
	float_area.Transparency = DEBUG and 0 or 1
	table.insert(float_areas, float_area)

	local anchor_part: BasePart = fan:WaitForChild("AnchorPart")
	local destination: Vector3 = anchor_part:GetAttribute("MoveDelta")
	if destination then
		anchor_part:SetAttribute("INITIAL_CFRAME", anchor_part.CFrame)
		table.insert(moving_anchor_parts, anchor_part)
	end
	
	CloneSound(script.Close, anchor_part)
	CloneSound(script.Far, anchor_part)
	CloneSound(script.Wind, float_area)
end

script.Parent.Models.ChildAdded:Connect(SetNewFan)
for i, fan: Model in script.Parent.Models:GetChildren() do	
	SetNewFan(fan)
end



--// Rotor spinning
local function AddNewRotor(rotor: BasePart)
	table.insert(rotors, rotor)
	rotor.Orientation += Vector3.new(0, 0, initial_time * 360 * ROTATIONS_PER_SECOND * rotor:GetAttribute("SpinMark") + math.random(0, 360))
end

COLLECTION_SERVICE:GetInstanceAddedSignal("FloatingFanRotor"):Connect(AddNewRotor)
for i, rotor: BasePart in COLLECTION_SERVICE:GetTagged("FloatingFanRotor") do
	AddNewRotor(rotor)
end



--// Detect character added
local function OnStateChanged(old_state: Enum.HumanoidStateType, new_state: Enum.HumanoidStateType)
	if new_state ~= Enum.HumanoidStateType.Freefall and float_animation.IsPlaying and not current_float_area then
		float_animation:Stop()
	end
end

local function SetNewCharacter(new_character: Model)
	if new_character then
		character = new_character
		humanoid = character:WaitForChild("Humanoid")
		root_part = character:WaitForChild("HumanoidRootPart")

		local animator: Animator = humanoid:WaitForChild("Animator")
		dive_animation = animator:LoadAnimation(script.DiveAnim)
		float_animation = animator:LoadAnimation(script.FloatAnim)

		vector_force.Attachment0 = root_part:WaitForChild("RootAttachment")

		humanoid.StateChanged:Connect(OnStateChanged)
	end
end

PLAYERS.LocalPlayer.CharacterAdded:Connect(SetNewCharacter)
SetNewCharacter(PLAYERS.LocalPlayer.Character)



--// Frame step
local function IsRootPartInCylinder(cylinder: BasePart)
	return (Vector2.new(root_part.Position.X, root_part.Position.Z) - Vector2.new(cylinder.Position.X, cylinder.Position.Z)).Magnitude <= cylinder.Size.Z / 2 and root_part.Position.Y <= cylinder.Position.Y + cylinder.Size.Y / 2 and root_part.Position.Y >= cylinder.Position.Y - cylinder.Size.Y / 2
end

local function OnPreRender(delta_time: number)
	initial_time += delta_time
		
	for i, anchor_part: BasePart in moving_anchor_parts do
		local initial_cframe: CFrame = anchor_part:GetAttribute("INITIAL_CFRAME")
		anchor_part.CFrame = initial_cframe:Lerp(initial_cframe + anchor_part:GetAttribute("MoveDelta"), (math.sin((workspace.DistributedGameTime + anchor_part:GetAttribute("MoveTimeOffset")) * anchor_part:GetAttribute("MoveSpeed")) + 1) * 0.5)
	end

	for i, rotor: BasePart in rotors do
		rotor.Orientation += Vector3.new(0, 0, delta_time * 360 * ROTATIONS_PER_SECOND * rotor:GetAttribute("SpinMark"))
	end

	if root_part then
		vector_force.Force = Vector3.new(0, root_part.AssemblyMass * (workspace.Gravity + FLOAT_ACCELERATION), 0)

		if float_animation.IsPlaying then
			float_animation:AdjustSpeed(1 + (math.clamp(math.abs(root_part.AssemblyLinearVelocity.Y), 0, 100) / 100) * 1.5)
		end

		if current_float_area then			
			if IsRootPartInCylinder(current_float_area) and humanoid.Health > 0 then				
				return
			end

			current_float_area = nil
			vector_force.Enabled = false
			humanoid.HipHeight = humanoid.RigType == Enum.HumanoidRigType.R15 and 2 or 0
		end

		for i, float_area: BasePart in float_areas do
			if IsRootPartInCylinder(float_area) then
				current_float_area = float_area

				humanoid.HipHeight = humanoid.RigType == Enum.HumanoidRigType.R15 and 0 or -2

				if not float_animation.IsPlaying then
					float_animation:Play(0.35)
					dive_animation:Play(0.2)
				end

				if root_part.AssemblyLinearVelocity.Y < 0 then
					root_part.AssemblyLinearVelocity = Vector3.new(root_part.AssemblyLinearVelocity.X, INITIAL_FALLING_VELOCITY, root_part.AssemblyLinearVelocity.Z)
				end

				vector_force.Enabled = true
				break
			end
		end
	end
end
RUN_SERVICE.PreRender:Connect(OnPreRender)
Old
--// Constants
local DEBUG = false -- Determines whether or not the float areas will be visible for the player.
local FLOAT_ACCELERATION = 100 -- How fast the player will accelerate upwards regardless of mass.
local INITIAL_FALLING_VELOCITY = -30 -- Sets player's vertical velocity to this value when they enter a float area while falling.
local ROTATIONS_PER_SECOND = 3 -- Spin frequency of the model's fan rotor parts.



--// Services
local COLLECTION_SERVICE = game:GetService("CollectionService")
local PLAYERS = game:GetService("Players")
local RUN_SERVICE = game:GetService("RunService")



--// Variables

-- Setup
local initial_time = 0

local anchor_parts = {}
local rotors = {}
local float_areas = {}

local function CloneSound(sound: Sound, parent: BasePart)
	sound = sound:Clone()
	sound.TimePosition = math.random(10, 100) / 10
	sound.Playing = true
	sound.Parent = parent
end

for i, fan: Model in script.Parent.Models:GetChildren() do	
	local float_area: BasePart = fan:WaitForChild("FloatArea")
	float_area.Transparency = DEBUG and 0 or 1
	
	local anchor_part: BasePart = fan:WaitForChild("AnchorPart")
	local destination: Vector3 = anchor_part:GetAttribute("MoveDelta")
	if destination then
		anchor_part:SetAttribute("INITIAL_CFRAME", anchor_part.CFrame)
	end
	
	CloneSound(script.Close, anchor_part)
	CloneSound(script.Far, anchor_part)
	CloneSound(script.Wind, float_area)
	
	table.insert(float_areas, float_area)
	table.insert(anchor_parts, anchor_part)
end

-- Floating
local character: Model
local humanoid: Humanoid
local root_part: BasePart

local dive_animation: AnimationTrack
local float_animation: AnimationTrack

local vector_force: VectorForce = script.VectorForce

local current_float_area: BasePart



--// Rotor spinning
local function AddRotor(rotor: BasePart)
	table.insert(rotors, rotor)
	rotor.Orientation += Vector3.new(0, 0, initial_time * 360 * ROTATIONS_PER_SECOND * rotor:GetAttribute("SpinMark") + math.random(0, 360))
end

COLLECTION_SERVICE:GetInstanceAddedSignal("FloatingFanRotor"):Connect(AddRotor)
for i, rotor: BasePart in COLLECTION_SERVICE:GetTagged("FloatingFanRotor") do
	AddRotor(rotor)
end



--// Detect character added
local function StateChanged(old_state: Enum.HumanoidStateType, new_state: Enum.HumanoidStateType)
	if new_state ~= Enum.HumanoidStateType.Freefall and float_animation.IsPlaying and not current_float_area then
		float_animation:Stop()
	end
end

local function CharacterAdded(new_character: Model)
	if new_character then
		character = new_character
		humanoid = character:WaitForChild("Humanoid")
		root_part = character:WaitForChild("HumanoidRootPart")
		
		local animator: Animator = humanoid:WaitForChild("Animator")
		dive_animation = animator:LoadAnimation(script.DiveAnim)
		float_animation = animator:LoadAnimation(script.FloatAnim)

		vector_force.Attachment0 = root_part:WaitForChild("RootAttachment")
		
		humanoid.StateChanged:Connect(StateChanged)
	end
end

PLAYERS.LocalPlayer.CharacterAdded:Connect(CharacterAdded)
CharacterAdded(PLAYERS.LocalPlayer.Character)



--// Frame step
local function IsRootPartInCylinder(cylinder: BasePart)
	return (Vector2.new(root_part.Position.X, root_part.Position.Z) - Vector2.new(cylinder.Position.X, cylinder.Position.Z)).Magnitude <= cylinder.Size.Z / 2 and root_part.Position.Y <= cylinder.Position.Y + cylinder.Size.Y / 2 and root_part.Position.Y >= cylinder.Position.Y - cylinder.Size.Y / 2
end

local function PreRender(delta_time: number)
	initial_time += delta_time
	
	for i, anchor_part: BasePart in anchor_parts do
		local initial_cframe: CFrame = anchor_part:GetAttribute("INITIAL_CFRAME")
		if initial_cframe then
			anchor_part.CFrame = initial_cframe:Lerp(initial_cframe + anchor_part:GetAttribute("MoveDelta"), (math.sin((workspace.DistributedGameTime + anchor_part:GetAttribute("MoveTimeOffset")) * anchor_part:GetAttribute("MoveSpeed")) + 1) * 0.5)
		end
	end
	
	for i, rotor: BasePart in rotors do
		rotor.Orientation += Vector3.new(0, 0, delta_time * 360 * ROTATIONS_PER_SECOND * rotor:GetAttribute("SpinMark"))
	end
	
	if float_animation.IsPlaying then
		float_animation:AdjustSpeed(1 + (math.clamp(math.abs(root_part.AssemblyLinearVelocity.Y), 0, 100) / 100) * 1.5)
	end
	
	if root_part then
		vector_force.Force = Vector3.new(0, root_part.AssemblyMass * (workspace.Gravity + FLOAT_ACCELERATION), 0)
		
		if current_float_area then			
			if IsRootPartInCylinder(current_float_area) and humanoid.Health > 0 then				
				return
			end
			
			current_float_area = nil
			vector_force.Enabled = false
			humanoid.HipHeight = humanoid.RigType == Enum.HumanoidRigType.R15 and 2 or 0
		end
		
		for i, float_area: BasePart in float_areas do
			if IsRootPartInCylinder(float_area) then
				current_float_area = float_area

				humanoid.HipHeight = humanoid.RigType == Enum.HumanoidRigType.R15 and 0 or -2
				
				if not float_animation.IsPlaying then
					float_animation:Play(0.35)
					dive_animation:Play(0.2)
				end

				if root_part.AssemblyLinearVelocity.Y < 0 then
					root_part.AssemblyLinearVelocity = Vector3.new(root_part.AssemblyLinearVelocity.X, INITIAL_FALLING_VELOCITY, root_part.AssemblyLinearVelocity.Z)
				end

				vector_force.Enabled = true
				break
			end
		end
	end
end
RUN_SERVICE.PreRender:Connect(PreRender)
1 Like