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)
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.
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.
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)
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.
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.