Jumping collisions with platforms

Right now, I’m trying to make a platform that you can jump through and then land on.
Sounds simple, but it really isn’t.

The two main issues I have currently is that it’ll make them colliding while I’m jumping through it making my character stuck in the platform, and that if I try to jump through another platform above the current it just won’t let me pass through at all.

The class I’m using is:

--// Dependencies
local PhysicsService = game:GetService("PhysicsService");
local RunService = game:GetService("RunService")

--// Constants
local PLATFORM_PART_NAME = "Platform";
local PLATFORM_COLLISION_GROUP_NAME = "PLATFORM_COLLISION_GROUP";
local PLAYER_COLLISION_GROUP_NAME = "%s_COLLISION_GROUP";
local MAX_RAY_DISTANCE = 10000;
local MAX_FALLING_WAIT_TIME = 1;

--// Variables
local Platforms = {};

--// Class
local JumpCollisions = {};
JumpCollisions.__index = JumpCollisions;

--// Functions
local function CollisionGroupExists(Name)
	for _, CollisionGroup in ipairs(PhysicsService:GetCollisionGroups()) do
		if (CollisionGroup.name == Name) then
			return true;
		end
	end
end

local function CreateIfNotCreated(Name)
	if (not CollisionGroupExists(Name)) then
		PhysicsService:CreateCollisionGroup(Name);
	end
end

local function BasePartIter(Parent)
	return coroutine.wrap(function()
		for _, BasePart in ipairs(Parent:GetDescendants()) do
			if (BasePart:IsA("BasePart")) then
				coroutine.yield(BasePart, BasePart.Name);
			end
		end
	end)
end

CreateIfNotCreated(PLATFORM_COLLISION_GROUP_NAME);
for BasePart, Name in BasePartIter(workspace) do
	if (Name == PLATFORM_PART_NAME) then
		table.insert(Platforms, BasePart)
		PhysicsService:SetPartCollisionGroup(BasePart, PLATFORM_COLLISION_GROUP_NAME);
	end
end

function JumpCollisions.Init(Player)
	local self = setmetatable({
		PlayerKey = string.format(PLAYER_COLLISION_GROUP_NAME, Player.Name);
		PlatformKey = PLATFORM_COLLISION_GROUP_NAME;
		Root = Instance.new("ObjectValue");
	}, JumpCollisions);
	
	CreateIfNotCreated(self.PlayerKey);

	local function SetCharacterParts(Character)
		if (not Player:HasAppearanceLoaded()) then
			Character = Player.CharacterAppearanceLoaded:Wait()
		end
		
		self.Root.Value = Character.HumanoidRootPart;
		
		for BasePart in BasePartIter(Character) do
			PhysicsService:SetPartCollisionGroup(BasePart, self.PlayerKey);
		end
	end
	
	if (Player.Character) then
		SetCharacterParts(Player.Character);
	end
	
	Player.CharacterAppearanceLoaded:Connect(SetCharacterParts);
	PhysicsService:CollisionGroupSetCollidable(self.PlayerKey, self.PlatformKey, true);
	
	return self;
end

function JumpCollisions:Start()
	local function StartRaycasting(Root)
		Root.Parent.Humanoid.StateChanged:Connect(function(_, New)
			local IsJumping = New == Enum.HumanoidStateType.Jumping;
			local IsFalling = New == Enum.HumanoidStateType.Freefall;
			local DownHit = self:Raycast("Up", true);
			
			if (IsJumping) then
				print("Made non-collidable");
				self:SetCollidable(false)
			elseif (IsFalling) then	
				if (not self:WaitUntilNotInPlatform()) then
					return;
				end
				print("Made collidable");
				self:SetCollidable(true);
			elseif (DownHit and PhysicsService:CollisionGroupContainsPart(self.PlatformKey, DownHit)) then
				print("Underneath is a Platform");
				self:SetCollidable(true);
			else
				self:SetCollidable(false);
			end
				
			print(string.rep("-", 50));
		end)
	end
	
	if (self.Root.Value) then
		StartRaycasting(self.Root.Value);
	end
	
	self.Root.Changed:Connect(StartRaycasting);
end

function JumpCollisions:Raycast(Direction, Negative)
	if (self.Root.Value) then
		local FullDirection = self.Root.Value.CFrame[Direction .. "Vector"] * MAX_RAY_DISTANCE;
		if (Negative) then
			FullDirection = -FullDirection;
		end
		local DirectionalRay = Ray.new(self.Root.Value.Position, FullDirection);
		
		return workspace:FindPartOnRay(DirectionalRay, self.Root.Value.Parent);
	end
end

function JumpCollisions:IsInPlatform()
	for _, Platform in ipairs(Platforms) do
		local Connection = Platform.Touched:Connect(function() end);
		for _, Colliding in ipairs(Platform:GetTouchingParts()) do
			if (Colliding:IsDescendantOf(self.Root.Value.Parent)) then
				Connection:Disconnect();
				return true
			end
		end
		Connection:Disconnect();
	end
end

function JumpCollisions:WaitUntilNotInPlatform()
	local TimeWaited = 0;
	while (self:IsInPlatform()) do
		TimeWaited = TimeWaited + RunService.Heartbeat:Wait();
		
		if (TimeWaited >= MAX_FALLING_WAIT_TIME) then
			return;
		end
	end

	return true;
end

function JumpCollisions:SetCollidable(Collidable)
	PhysicsService:CollisionGroupSetCollidable(self.PlayerKey, self.PlatformKey, Collidable);
end

return JumpCollisions;

The Start function mainly handles it, with Raycast as my primary raycasting function and WaitUntilNotInPlatform as my function to check that I’m not inside the Platform - probably the issue.

The code which inits this is:

--// Dependencies
local Players = game:GetService("Players");
local ServerStorage = game:GetService("ServerStorage");

--// Modules
local JumpCollisionsModule = require(ServerStorage.Modules.JumpCollisions);

--// Functions
local function PlayerAdded(Player)
	local JumpCollisions = JumpCollisionsModule.Init(Player);
	JumpCollisions:Start();
end

--// Connections
Players.PlayerAdded:Connect(PlayerAdded)

Possibly not the issue at hand though.

Just note that I have a DoubleJump client-sided class that handles double jumping, when I double jump I can move through the platform.

If any extra information is required then I’ll be happy to provide it.

2 Likes

Not a solution to your issue at all but, if you’re interested, I have an unefficient? (and slightly dumb yet simple) method for such platforms.

2 Likes

I don’t understand what you are trying to do. Make a platform you can jump through from below but can land on top of?

Exactly what I’m trying to achieve here.

I see. What you can do (from a localscript) on Touched is see if the position of their feet (I assume R15?) is above or below the part. If below, then CanCollide = false. If above, CanCollide = true.

Maybe put every of those platforms in a folder in workspace and in a localscript in PlayerScripts do for _, v in ipairs (folder:GetChildren) do v.Touched:Connect()

Thanks but that’s essentially what @Triaxiality posted, I’m surprised that I didn’t think of it before :thinking:

What if you tried tracking the Player’s y velocity instead of animation states? When the humanoid is jumping (and moving upwards) their y velocity should be > 0, but if they’re falling or standing it should be <= 0.

I haven’t tried this before, so I don’t know how effective it would be, but it feels like it would work. If you’re having trouble with the player getting stuck inside of platforms (i.e the apex of their jump is halfway inside the block) you could detect if the character is touching a certain platform and allow them to fall through before changing the collisions on that part.

Of course simplicity beats my advanced method, works all great now with the use of CollectionService as well.
Here’s the class if anyone wants it:

--// Dependencies
local Players = game:GetService("Players");
local RunService = game:GetService("RunService");
local CollectionService = game:GetService("CollectionService");

--// Instances
local LocalPlayer = Players.LocalPlayer;

--// Constants
local PLATFORM_PART_NAME = "Platform";
local PLATFORM_PART_TAG = "PLATFORM";

--// Class
local JumpCollisions = {};
JumpCollisions.__index = JumpCollisions;

--// Functions
local function BasePartIter(Parent)
	return coroutine.wrap(function()
		for _, BasePart in ipairs(Parent:GetDescendants()) do
			if (BasePart:IsA("BasePart")) then
				coroutine.yield(BasePart);
			end
		end
	end)
end

--// Class functions
function JumpCollisions.Init()
	local self = setmetatable({
		RightFoot = nil;
	}, JumpCollisions);
	
	for BasePart in BasePartIter(workspace) do
		if (BasePart.Name == PLATFORM_PART_NAME) then
			CollectionService:AddTag(BasePart, PLATFORM_PART_TAG);
		end
	end
	
	local function SetRightFoot(Character)
		self.RightFoot = Character:WaitForChild("RightFoot");
	end
	
	if (LocalPlayer.Character) then
		SetRightFoot(LocalPlayer.Character);
	end
	
	LocalPlayer.CharacterAdded:Connect(SetRightFoot);
	
	return self;
end

function JumpCollisions:Start()
	RunService.Heartbeat:Connect(function()
		for _, Platform in ipairs(CollectionService:GetTagged(PLATFORM_PART_TAG)) do
			if (self.RightFoot and Platform.Position.Y < self.RightFoot.Position.Y) then
				Platform.CanCollide = true;
			else
				Platform.CanCollide = false;
			end
		end
	end)	
end

return JumpCollisions;
1 Like

Absolutely useful. Thanks a ton, even a few months after the original post.