Floating Fan Platforms

Heya!

I decided to make a model based on the floating fan platforms from the Sega Sonic series’ Wing Fortress / Flying Battery levels.


I made it for practice at first, but I think it can benefit other people, so I made it public.
The functionality is purely local, so if you want it to work with server-related things like NPCs, I recommend setting up a script for that.

Finished in 2 days, hope you enjoy!
> Marketplace
> Test place

Pictures




19 Likes

Wow, I mean… WOW!
I absolutely love this!

I tested it out in my game (Works very well), but I DID want to ask 1 question.

I know that the script is local inside of workspace. But I use Knit framework and was wondering if you could help me figure out why it isnt working.

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

local FloatingFansFolder = workspace.FloatingFans

--//Modules
local Modules = ReplicatedStorage.Modules

--//Assets
local Assets = ReplicatedStorage:WaitForChild("Assets")
local Animations = Assets.Animations
local Sounds = Assets.Sounds

--//Animations
local FloatingFanAnimations = Animations.Universal.Interaction.FloatingFan

local DiveAnimation = FloatingFanAnimations.Dive
local FloatAnimation = FloatingFanAnimations.Float

--//Sounds
local FloatingFanSounds = Sounds.Universal.Interaction.FloatingFan

local CloseSound = FloatingFanSounds.Close
local FarSound = FloatingFanSounds.Far
local WindSound = FloatingFanSounds.Wind

--//Variables
local DEBUG = false 
local FLOAT_ACCELERATION = 100 
local INITIAL_FALLING_VELOCITY = -30 
local ROTATIONS_PER_SECOND = 3 

--//Setup
local initial_time = 0

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

local vector_force: VectorForce = script.VectorForce

local current_float_area: BasePart

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

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

--//Start
function FloatingFanController:KnitStart()
	
	vector_force.Attachment0 = Knit.Player.Character.HumanoidRootPart:WaitForChild("RootAttachment")
	
	for i, rotor: BasePart in CollectionService:GetTagged("FloatingFanRotor") do
		AddRotor(rotor)
	end

	CollectionService:GetInstanceAddedSignal("FloatingFanRotor"):Connect(AddRotor)
	
	Knit.Player.Character.Humanoid.StateChanged:Connect(StateChanged)
	
	RunService.PreRender:Connect(PreRender)
	
	for i, fan: Model in FloatingFansFolder: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(CloseSound, anchor_part)
		CloneSound(FarSound, anchor_part)
		CloneSound(WindSound, float_area)

		table.insert(float_areas, float_area)
		table.insert(anchor_parts, anchor_part)
	end

end

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

function StateChanged(old_state: Enum.HumanoidStateType, new_state: Enum.HumanoidStateType)
	if new_state ~= Enum.HumanoidStateType.Freefall and Knit.GetController("AnimationController"):IsPlaying(FloatAnimation) == true and not current_float_area then
		Knit.GetController("AnimationController"):StopAnimation(FloatAnimation)
	end
end

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

function IsRootPartInCylinder(cylinder: BasePart)
	return (Vector2.new(Knit.Player.Character.HumanoidRootPart.Position.X, Knit.Player.Character.HumanoidRootPart.Position.Z) - Vector2.new(cylinder.Position.X, cylinder.Position.Z)).Magnitude <= cylinder.Size.Z / 2 and Knit.Player.Character.HumanoidRootPart.Position.Y <= cylinder.Position.Y + cylinder.Size.Y / 2 and Knit.Player.Character.HumanoidRootPart.Position.Y >= cylinder.Position.Y - cylinder.Size.Y / 2
end

function PreRender(delta_time: number)

	local Character: Model = Knit.Player.Character
	local RootPart = Knit.Player.Character.HumanoidRootPart
	local Humanoid: Humanoid = Knit.Player.Character.Humanoid
	local Animator: Animator = Knit.Player.Character.Humanoid.Animator

	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 Knit.GetController("AnimationController"):IsPlaying(FloatAnimation) == true then 
		Knit.GetController("AnimationController"):AdjustSpeed(FloatAnimation, 1 + (math.clamp(math.abs(RootPart.AssemblyLinearVelocity.Y), 0, 100) / 100) * 1.5)
	end

	if RootPart then
		vector_force.Force = Vector3.new(0, RootPart.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 Knit.GetController("AnimationController"):IsPlaying(FloatAnimation) == false then 
					Knit.GetController("AnimationController"):PlayAnimation(FloatAnimation)
					Knit.GetController("AnimationController"):PlayAnimation(DiveAnimation)
				end

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

				vector_force.Enabled = true
				break
			end
		end
		
	end
	
end



return FloatingFanController

If you have any questions let me know!

I do plan to modularize it to only use collection service and some other adjustments + possible performance improvements (If I can).

I didnt want to mess around too much since you would obviously know more about this then me.

2 Likes

I haven’t heard about Knit before, but I assume it takes care of character-related stuff. The issue could be that it doesn’t update the variables properly, like the vector force’s Attachment0 being set to RootAttachment only when :KnitStart() is called.

The script can be placed anywhere you want, as long as it’s not cloned again and it has access to the fan models, because it should only set them up once. You just have to make sure the needed variables reset when the player gets a new character.

Also, does it print any errors in the output?

No, thankfully.

Knit handles updates internally, this isnt the issue and I checked it thankfully.

1 Like

It seems that the line that sets the vector force’s Attachment0 property only runs once, though. Every time the player resets, it needs to change to the new attachment. That also includes the Humanoid’s StateChanged event. It stops firing its signal once the Humanoid is destroyed, which is why the StateChanged function needs to be relinked.

Ah I see, I will try this once I can.

I was thinking of using Zone Instead of the “IsRootPartInCylinder” function to highly increase performance.

1 Like

Ah sorry, it seems that I forgot to mention what the bug even was.

Everything is in order… The animation playing, detecting when the character has landed and stopping the animation. And the parenting of said vector force. However the character doesnt go up. It seems that velocity or force isnt being changed or something.

1 Like

Are u using a custom character or starter character?

Startercharacter, R6 with standard humanoid

Got it working, turns out the problem was that the vectorforce didnt exist in workspace so its behaviour was changed. All you have to do is parent it to the rootattachment and it works perfectly.

Ill work on increasing performance (Especially detecting if the player is inside the floating fan radius’s. Using zone and a couple of adjusments to the code itself.

Thank you for your help!

1 Like

If the zone will increase performance let me know, I might integrate it or make something similar!

pretty awesome!!!

1 Like

Yes, trying to implement it as we speak.

1 Like

I’m (saintimmor on my phone)

So problem.

I haven’t used zone in a long time. Turns out since its last update was in (sep 9 2021). It’s basically completely broken.

Trying to create my own module would take way too much time. So for now I will wait for someone to eventually create an alternative.

Until then I will work on modularizing the code. And increasing the performance of other parts of code.

1 Like

I’m a rotating blade of this resource.

(Fan)

1 Like

This is infact me lol.

As I said, zone isnt currently working.

However, I found an alternative method that doesnt require a module. Will be using it now haha.

1 Like

Zone has a significant memory leak ever look at it via the script profiler? Also it uses a lot of CPU compared to other methods, I’ve used it, tested Zone, implemented it and had to rip it out because it doesn’t scale very well either. The memory leak is an array called ‘userData’ . It also declares the entire Enum library as a local variable. Issues like that I noticed from looking at the memory usage and script CPU percentages.

Also, Concerning this post, I would suggest this change to the local script code to prevent memory leaks starting at line 105.

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

local function RemoveRotor(rotor: BasePart)
	local index=rotor:GetAttribute("Index")
	if index then
		table.remove(rotors,index)
	end
end
COLLECTION_SERVICE:GetInstanceRemovedSignal("FloatingFanRotor"):Connect(AddRotor)

COLLECTION_SERVICE:GetInstanceAddedSignal("FloatingFanRotor"):Connect(AddRotor)

This would make your fans compatible with streaming enabled.

1 Like

The modules run in a local enviroment.

Anything I could use to replace it? Ive been trying but I havent been able to :frowning: .

Depends on what you use it for. What I did is just code actual functions to do the thing I need it to do. I was using it for my Renderer because I thought it was supposed to be efficient but it had significant CPU usage issues so I just wrote in the distance checks starting with larger zones so there is a grid of areas which it queries first to find the closest part (make for a lot less distance queries) then, those objects in that localized area are checked for distance and if it is in view of the camera.

1 Like

Hm I see.

My use case is checking if the player is inside the floating fan platforms zone.

I use a variable InFloatingFanCylinder: Boolean to check if the animations and velocity should be active in a renderstepped loop:

local Character: Model = Knit.Player.Character
	local RootPart = Knit.Player.Character.HumanoidRootPart
	local Humanoid: Humanoid = Knit.Player.Character.Humanoid
	local Animator: Animator = Knit.Player.Character.Humanoid.Animator

	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 Knit.GetController("AnimationController"):IsPlaying(FloatAnimation) == true then 
		Knit.GetController("AnimationController"):AdjustSpeed(FloatAnimation, 1 + (math.clamp(math.abs(RootPart.AssemblyLinearVelocity.Y), 0, 100) / 100) * 1.5)
	end

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

		if InFloatingFanCylinder == true then

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

			if Knit.GetController("AnimationController"):IsPlaying(FloatAnimation) == false then 
				Knit.GetController("AnimationController"):PlayAnimation(FloatAnimation)
				Knit.GetController("AnimationController"):PlayAnimation(DiveAnimation)
			end

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

			vector_force.Enabled = true

		elseif InFloatingFanCylinder == false then

			if Humanoid.Health > 0 then				
				return
			end

			vector_force.Enabled = false
			Humanoid.HipHeight = Humanoid.RigType == Enum.HumanoidRigType.R15 and 2 or 0

		end

	end

I change this variable using zone

	local CylinderZone = Zone.new(FloatingAreaTagged)

	CylinderZone.localPlayerEntered :Connect(function()
		InFloatingFanCylinder = true
		print("ZoneEntered")
	end)

	CylinderZone.localPlayerExited:Connect(function()
		InFloatingFanCylinder = false
		print("ZoneExited")
	end)

Think there is some sort of premade alternative to zone? Im a LITTLE to lazy to code something like it right now honestly.

1 Like