Terrain:ReadVoxels(): Detecting an underwater player

I’m trying to detect if a player is underwater or not at any given moment, and I have decided to use Terrain:ReadVoxels() on the HumanoidRootPart’s region and attain a material for the same (feel free to PM me if there are other better ways to do so, this post talks about the case of using ReadVoxels). As of now I have this part of the script handling the case:

local RunService = game:GetService("RunService")
local player = game.Players.LocalPlayer

RunService:BindToRenderStep("Topbar", 101, function()
	local character = player.Character
	if not character then return end
	local root = character.PrimaryPart
	if not root then return end
	local rootPos = root.Position
	local increment = root.Size / 2
	local region = Region3.new(rootPos - increment, rootPos + increment)
	region:ExpandToGrid(4)
	local material, occupancy = workspace.Terrain:ReadVoxels(region, 4)
	material = material[rootPos.X][rootPos.Y][rootPos.Z]
	if material == Enum.Material.Water then
		TweenColor("Water") -- The script never reaches this part, so I haven't shared the place where it's defined
	else
		TweenColor("Land")
	end
end)

This is the error I am recieving:


The first time I got this error, I looked up Region3:ExpandToGrid(), which says the function takes in a Region3 and a resolution (which in my case is 4, consistent with the ReadVoxels resolution) and returns the Region3 aligned to the voxel grid. However, after implementing the same in the script, the error doesn’t seem to be resolved.
I might have overlooked something but I can’t find what’s wrong, I’d appreciate any help at all for this.

Regards,
PirateOnThePlank

2 Likes

Try using Humanoid:GetState()

1 Like

Hello! :wave:

The issue here indeed lies within your usage of region:ExpandToGrid(). Using region:ExpandToGrid() on an already existing region3 dosen’t actually do anything with it, instead it returns a new region3, which is what you’ll want to use. To resolve this issue, simply define region as region:ExpandToGrid(4), or use a new variable:

local region = Region3.new(rootPos - increment, rootPos + increment)
local expandedregion = region:ExpandToGrid(4)

As PaienCommuniste already has pointed out, a much easier way to compute whenever the player is swimming or not is by using the :GetState() function of Humanoids. However, in most situations I’d recommend using the Humanoid.StateChanged instead as it’s an event, meaning your code only needs to be run every time it fires and not every single frame.

In conclusion; Use Humanoid.StateChanged!

7 Likes

StateChanged often gives incorrect results as it will fire rapidly if the player is on surface.

The original idea he had was much better.

Humanoid.StateChanged worked perfectly! Small checks for the old and new states were enough for it, thanks :smile:
https://gyazo.com/d156b7a8160c6605211ed9aecf3764bf

The topbar is what changes

player.CharacterAdded:Connect(function(character)
		print(character.Name.." loaded!")
		local humanoid = character:WaitForChild("Humanoid", 10)
		humanoid.StateChanged:Connect(function(oldState, newState)
			if newState == Enum.HumanoidStateType.Swimming then
				TweenColor("Water")
			elseif oldState == Enum.HumanoidStateType.Swimming and not newState == Enum.HumanoidStateType.Jumping then
				TweenColor("Land")
			elseif oldState ~= Enum.HumanoidStateType.Swimming and oldState ~= newState then
				TweenColor("Land")
			end
		end)
	end)
1 Like

While this might be an issue when it comes to being on land, there is only one state for swimming. To prove that this works and how using events are far more resource efficient I quickly threw together a demo:

As you can see, both loops and events are able to detect if the player is swimming or not. Also, note how the amount of iterations are severely higher when using loops.

File: Example.rbxl (19.7 KB)

Try holding space while in water. Most players will keep themselves above the surface. States were only added for animation groups, meaning they will constantly change. Reading voxels each frame, while not being the best solution to this, still manages to fix the issue. You can read the current voxel for water then read the one under in case the first isn’t. The best solution would be to combine state with ray casting. Cast a ray downwards with a short distance, white-listing only water in case the state isn’t Swimming.