How to check if player is in the air for an air defense system

You can write your topic however you want, but you need to answer these questions:

  1. What do you want to achieve? Keep it simple and clear!

I want to be able to detect if players are in midair, without it passing though floorboards and platforms

  1. What is the issue? Include screenshots / videos if possible!

I can figure out how to do this properly

  1. What solutions have you tried so far? Did you look for solutions on the Developer Hub?
    Ive tried looking through the devforums, but no one has asked a similar question as far as I know

Does anyone know how to do this?

Once you have a reference to the player’s Character, look for its Humanoid. From there, you can check if the Humanoid.FloorMaterial property is set to Enum.Material.Air, as that indicates that the player is not standing on anything.

if Humanoid.FloorMaterial == Enum.Material.Air then
    -- Continue
end
1 Like

I have this in the script, but this also works for when the player jumps and/or if theyre junping on an elevated platform. I want to be able to check if theyre free falling or flying in the air

1 Like

In that case, you can check the Humanoid’s HumanoidStateType by calling Humanoid:GetState(), or by listening for it to change with the Humanoid.StateChanged event.

There’s a HumanoidStateType specifically called FreeFall, which indicates that:

The Humanoid is currently freefalling (jumped from a height or fell off a ledge).

However, this would also apply when a player is falling after jumping (which could include falling from an elevated platform). If I understand correctly, you don’t want to detect that kind of situation?*


But for flying in the air, do you mean flying in a vehicle or the Character model is independently flying in the air (such as with a fly tool)? If it’s the latter, you could check if the HumanoidStateType happens to be set to Enum.HumanoidStateType.Flying.

If a player is jumping inside an elevated platform or inside a building and they jump, I do not want to detect it

1 Like

What about the second question:


And if you don’t mind, could you explain more about the specific conditions that you want to check for before having the script confirm that the player is “midair” or “flying”?

For example:

  • Does the player need to be completely outdoors (nothing directly above their Character except for the sky)?

  • Would it be possible for the player to be in a flying vehicle?

    • If so should the script be able to detect any players that are midair in a flying vehicle?
  • When freefalling, does the player need to be falling for a certain amount of time before being detected?

  • Does the player need to be freefalling / flying above a certain height in the world (on the Y Axis)?

  • Is there a height level / range where players should not be detected by the script for freefalling / flying (for example, anything below 25 studs on the Y Axis would not be detected, even if all of the other conditions were true)?

There are some possible solutions I could think of if at least one of those is a potential factor you would like for the script to consider before it declares that a player is currently midair or flying. However, as I have not created something like this before, I may be limited with how much I could explain about it.

  1. The player does not need to be completely outdoors, there can be something above them, just not under

  2. There will most likely be flying vehicles of a kind, but for now I only want to detect if theyre falling or in the air

  3. There does not need to be a certain time for the player to be falling, just as long as theyre in the air

As for the positioning and range, that is already made, I just need to fix the jumping and falling issue

1 Like

I think there will have to be a ‘certain time’ if you don’t want to jumping to be detected.

The only way around this that I can think of is maybe a raycast down(to identify jumping ground proximity), seems like overkill though.

1 Like

There is a cooldown for shooting and right before it starts for 0.8 seconds

As for raycasting, it feels overcomplicated and I feel like there is a better solution

Based on those factors, the only option I can think of at the moment would be to utilize raycasting in combination with previously mentioned events / methods. This is because the aforementioned FloorMaterial, HumanoidStateType, and positional / height checks would probably not be able to determine the full context that a player is mid-air (essentially, not knowing whether or not their Character is indoors). Furthermore:

  • Although you could integrate UserInputService.JumpRequest to ignore situations where a player requests to jump, that could be abused since the JumpRequest event can be fired repeatedly while the Character is mid-air.

  • You could use the Humanoid.Jumping event or manually listen for the value of Humanoid.Jump to change to true (as it is momentarily set to true when a Character successfully jumps, and is set back to false until the Character successfully jumps again), but this would not be able to detect if a player jumped from a high altitude, is not indoors, and the amount of time it takes until they land on a surface.

    • (This also happens to be very similar to listening for the HumanoidStateType of Jump)

However, if you did not want to use raycasting and were to theoretically have a certain amount of time that a player would need to be mid-air before it detects the player, you may be able to use a combination of the RunService and listening for the Humanoid.StateChanged event.

Before fully creating that system, you could use the RunService to measure the amount of time it takes, on average, for a Character’s HumanoidStateType to go from Jump to Landed or Running, when the player’s Character is jumping on a flat surface (which would differ based on the JumpHeight / JumpPower settings for the Characters in your game). With that baseline, you could store a number that is slightly higher than that to ensure that it would not detect players who are mid-air for the average amount of time it takes to jump while indoors.

In the actual system, you could use Humanoid.StateChanged to check when a player’s Character enters the Freefall or Flying state and then begin tracking the amount of time elapsed (through the RunService) until the event fires again with the new HumanoidStateType being set to Landed or Running. If it exceeds the time threshold before the HumanoidStateType is set back to Landed or Running, it is more likely that the Character has not jumped or fallen while indoors.

Unfortunately, without raycasting, this would also have its flaws, as players may be able to jump from elevated surfaces while indoors and still be able to exceed that time threshold.



One more note about the Raycasting option…

If you decide to go with the raycasting option, it would be possible to optimize its usage by only creating raycasts during necessary situations. For instance, when utilizing a combination of FloorMaterial, HumanoidStateType, and positional / height checks, you could raycast downwards from the Character if:

  • The FloorMaterial is Enum.Material.Air
  • HumanoidStateType is set to either Jumping, Freefall, or Flying
  • The Character meets the in-world positional / height requirements that you’ve set

If all of those conditions are true, upon a successful downwards raycast, you could check if the RaycastResult.Distance exceeds a desired threshold that you deem to be a large enough distance to indicate that a player is mid-air without being inside of a building. For example, if the distance returned by the Raycast is only 10 studs, the player is probably indoors. If the distance is 100 studs, on the other hand, that would indicate that there is nothing between the Character model and the world below them for a distance that is much more likely to only be possible if the Character is outdoors.


(Note that the filtering of the Raycast would also need to be set up in a way where it ignores Instances that should not be considered a walkable surface. For instance, situations where one player’s Character is directly above another player’s Character, or, eventually, the surface of another player’s flying vehicle, could lead to erroneous results that incorrectly determine that a player is freefalling or flying while being very close to a building, the ground, etc.)

  • Extra note: If you add flying vehicles into your game in the future, it will be much easier to accurately determine situations where a player is flying a vehicle without needing to use Raycasts, the RunService, etc., since you could check if Humanoid.Seated is true or if their HumanoidStateType is set to Seated, and then check if Humanoid.SeatPart returns a Seat that is in one of the flying vehicles, in addition to checking any positional / height requirements you’ve established to ensure it doesn’t detect the player as being mid-air while piloting a flying vehicle that is still on the ground.

Based on the conditions that you have mentioned, those are the possible solutions I can think of right now. Keep in mind that there may be other solutions that I am currently unaware of.

I hope that you will be able to find a suitable solution that matches the needs for your game!

The Humanoid.StateChanged function could be useful, but how would I go to implement this?

Edit: I do not think there would be a platform elevated enough indoors to consider that it could not work

Here’s the code btw if you want to look at it:

local model = script.Parent
local minimumHeight = 13
local range = 30

while task.wait(0.8) do
	for i, v in ipairs(workspace:GetChildren()) do
		if v:IsA("Model") and v:FindFirstChild("Humanoid") then
			local char = v
			local root = char:FindFirstChild("HumanoidRootPart")
			local humanoid = char:FindFirstChild("Humanoid")
			
			if (root.Position.Y - model.PrimaryPart.Position.Y) >= model.PrimaryPart.Position.Y + minimumHeight then
				if math.abs(root.Position.X - model.PrimaryPart.Position.X) <= model.PrimaryPart.Position.X + range then
					if math.abs(root.Position.Z - model.PrimaryPart.Position.Z) <= model.PrimaryPart.Position.Z + range then
						if humanoid.FloorMaterial == Enum.Material.Air then
							local rocket = model.Rocket:Clone()
							local rotate = rocket.Rotate
							local move = rocket.Move
							
							rocket:SetAttribute("followPlayer", char.Name)
							rocket.Parent = workspace

							rotate.Enabled = true
							move.Enabled = true

							rocket.rocketMesh.Transparency = 0
						end
					end
				end
			end
		end
	end
end
1 Like

To clarify, since both of the possible implementations for RunService and Raycasting that I mentioned would make use of the Humanoid.StateChanged event in some way, which option are you leaning more towards?

*Side note: I will likely be away from my computer for the next few hours so I may not respond for a while

RunServuce seems like the better option in this case, how would I do this?

1 Like

I’ve spent a while experimenting with possible ways of implementing that into your existing system, and I created this proof of concept that should be easily scalable if your game has multiple turrets included in the defense system.

It’s very very late where I am and I’m fairly tired at the moment, so if there’s anything I need to explain further, feel free to let me know and I’ll respond after I’ve gotten some rest.

Example Revision

local RunService = game:GetService("RunService")
local Players = game:GetService("Players")

-- Settings

local airTimeThreshold = 0.6
local desiredTurretFireWaitTime = 0.8

local midairStates = {
	Enum.HumanoidStateType.Jumping,
	Enum.HumanoidStateType.Freefall,
	Enum.HumanoidStateType.Flying
}

local endStates = {
	Enum.HumanoidStateType.Landed,
	Enum.HumanoidStateType.Running
--[[ Consider adding Enum.HumanoidStateType.Climbing, too, since it is
possible to enter that state if a player begins climbing a
ladder / truss while falling down, rather than entering the
"Landed" state. However, a reason to not add this as one of the
"endStates" would be to avoid situations where players somehow
enter this state while being mid-air and in-range of a turret.
I figured I would mention this just in case.
--]]
}

--

local existingTurrets = {} --[[ Add all turret models in the defense system
into this table. This will make it possible to have one script that runs for
every single turret rather than having a separate script for each turret.

If you need to customize each turret's "minimumHeight" and "range" threshold,
then I would recommend adding those as attributes to the turret model so that
the loop in the "activateRocket" function can reference the attribute
when checking for those conditions.
--]]

-- Example
local turretsFolder = workspace:FindFirstChild("Turrets")

if turretsFolder then
	
	local function updateTurretTable(turret, operation)
		if turret:IsA("Model") then
			
			local tableCheck = table.find(existingTurrets, turret)
			if tableCheck and operation == false then
				table.remove(existingTurrets, tableCheck)
				
			elseif not tableCheck and operation == true then
				table.insert(existingTurrets, turret)
			end
			
			warn(existingTurrets)
		end
	end
	
	for _, turret in turretsFolder:GetChildren() do
		updateTurretTable(turret, true)
	end
	
	turretsFolder.ChildAdded:Connect(function(turret)
		updateTurretTable(turret, true)
	end)
	
	turretsFolder.ChildRemoved:Connect(function(turret)
		updateTurretTable(turret, false)
	end)
	
end



-- Main Functions

local currentPlayers = {}

local function disconnectConnections(Table)
	
	for _, connection in Table do
		if typeof(connection) == "RBXScriptConnection" then
			connection:Disconnect()
			connection = nil
		end
	end
	
end

local function activateRocket(Character)
	warn("Activating Rocket")
	local minimumHeight = 13
	local range = 30
	
	local Humanoid = Character:WaitForChild("Humanoid")
	local characterCFrame = Character:GetPivot()
	local characterPosition = characterCFrame.Position
	
	for _, model in existingTurrets do
		local primaryPart = model.PrimaryPart
		local primaryPartPosition = primaryPart.Position
		
		local heightThreshold = primaryPartPosition.Y + minimumHeight
		
		if characterPosition.Y < heightThreshold then warn("Condition1 Failed") return end
		
		local subtractedPositions = characterPosition - primaryPartPosition
		if math.abs(subtractedPositions.Magnitude) > range then warn("Condition2 Failed") return end
		
		local rocket = model.Rocket:Clone()
		local rotate = rocket.Rotate
		local move = rocket.Move
		
		rocket:SetAttribute("followPlayer", Character.Name)
		rocket.Parent = workspace
		
		rotate.Enabled = true
		move.Enabled = true
		
		rocket.rocketMesh.Transparency = 0
	end
end


local function onCharacterSpawn(player, Character)
	
	local playerTable = currentPlayers[player]
	local currentlyMidair = false
	
	local Humanoid = Character:WaitForChild("Humanoid")
	playerTable["StateChangedConnection"] = Humanoid.StateChanged:Connect(function(oldState, newState)
	
		if table.find(midairStates, newState) then
			
			if currentlyMidair == true then return end
			
			currentlyMidair = true
			local currentAirTime = 0
			
			local preparingToFire = false
			local currentWaitTime = 0
			
			playerTable["RunServiceConnection"] = RunService.Heartbeat:Connect(function(deltaTime)
				currentAirTime += deltaTime
				
				--print(currentAirTime)
				
				if currentAirTime >= airTimeThreshold then
					warn("alert!")
					
					if preparingToFire == false then
						preparingToFire = true
						currentWaitTime = currentAirTime
					end
					
					currentWaitTime += deltaTime
					if currentWaitTime >= desiredTurretFireWaitTime then
						currentWaitTime -= desiredTurretFireWaitTime
						activateRocket(Character)
					end
				end
			end)
			
		elseif table.find(endStates, newState) then
			disconnectConnections({playerTable["RunServiceConnection"]})
			currentlyMidair = false
			print("Disconnected RunService connection")
		end
	end)
	
end

local function onPlayerJoin(player)
	
	currentPlayers[player] = {}
	
	if player.Character then
		onCharacterSpawn(player, player.Character)
	end
	
	player.CharacterAdded:Connect(function(Character)
		onCharacterSpawn(player, Character)
	end)
	
	player.CharacterRemoving:Connect(function()	
		local playerTable = currentPlayers[player]
		disconnectConnections(playerTable)
	end)
	
end

for _, player in Players:GetPlayers() do
	task.spawn(onPlayerJoin, player)
end

Players.PlayerAdded:Connect(onPlayerJoin)

Players.PlayerRemoving:Connect(function(player)
	currentPlayers[player] = nil
end)

Could I possibly remove the turretsFolder and existingTurrets table?

1 Like

You could; that was an example of a possible way you could have an individual script perform a distance check for all of the turrets at once instead of having separate scripts do that.

If you remove that section, the only function you would need to adjust in the rest of the code would be the activateRocket function, since the loop would no longer be necessary. If you only want to perform the check for an individual turret, you could have the model variable reference one specific turret you want to activate, just like in the original code you provided.

Example revision of the “activateRocket” function

local function activateRocket(Character)
	warn("Activating Rocket")
	local minimumHeight = 13
	local range = 30
	
	local Humanoid = Character:WaitForChild("Humanoid")
--[[ *I honestly don't remember why I included a variable for the Humanoid here.
This could probably be removed from the function, too, since it's unused --]]

	local characterCFrame = Character:GetPivot()
	local characterPosition = characterCFrame.Position

    local model = -- Reference to the turret model here
	local primaryPart = model.PrimaryPart
	local primaryPartPosition = primaryPart.Position
		
	local heightThreshold = primaryPartPosition.Y + minimumHeight
		
	if characterPosition.Y < heightThreshold then warn("Condition1 Failed") return end
		
	local subtractedPositions = characterPosition - primaryPartPosition
	if math.abs(subtractedPositions.Magnitude) > range then warn("Condition2 Failed") return end
		
	local rocket = model.Rocket:Clone()
	local rotate = rocket.Rotate
	local move = rocket.Move
		
	rocket:SetAttribute("followPlayer", Character.Name)
	rocket.Parent = workspace
		
	rotate.Enabled = true
	move.Enabled = true

	rocket.rocketMesh.Transparency = 0
end

I used your code and adjusted the tables, the warnings are showing and the print statements are working, but nothing is firing for some reason?

Even when I reduced the cooldowns to 0 it still wouldn’t shoot

Make sure the code is being run on the server-side. And also, when you said that the warnings were appearing, which ones were they?

  • If “Activating Rocket” appeared, that means that the activateRocket function is running.
  • If “Condition1 Failed” appeared, that means that the Character is not above the height threshold.
  • If “Condition2 Failed” appeared, that means that the Character is not within the defined distance range.

I’d recommend adding an additional print statement at the end of the activateRocket function (and possibly removing some of the other ones throughout the script if the Output is getting too cluttered) so it’s easier to know when the Character is completely within range of the turret and a rocket is being cloned and added into the Workspace.

It would Disconnect RunService connection (as I spawned)
Alert! (As I would fall)
And Disconnect RunService connection again as I landed

If it never warned “Activating Rocket” (which should appear in the Output every time the activateRocket function runs if it wasn’t removed from the code), then the Character had not continuously been in any of the midairStates (Jumping, Freefall, or Flying) for a duration of time that equalled or exceeded the amount of time defined by airTimeThreshold at the top of the script.

Edit: Either that, or something went wrong with the currentWaitTime >= desiredTurretFireWaitTime check; however, you mentioned that you set that to 0, which means that the function should run immediately after it prints “alert!”

To better understand what could be happening, what does the full script look like now, after you modified it?



Closing thoughts from the original draft of this post, before the edits & before realizing you said you set the cooldowns to 0

The airTimeThreshold is purposely set to a duration of time where the activateRocket function would never be activated if a player jumps on a flat surface, so make sure that during your tests, the Character is falling for a long enough period of time to ensure that the activateRocket function is called.

Or, you could temporarily lower the airTimeThreshold to test the functionality of the activateRocket function while the Character is jumping in place while also being in range of the turret, and then set the threshold back to the desired value after you’re done testing it.