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




8 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!