Raycasting problem for platformer (lags behind?)

I’ve been stuck on this bug for over a week, and I’ve hit a wall. Total mental block on this one!
The problem is with my raycasting, which I’m using for all player collision. Some of the raycasting acts as if it’s lagging behind by a frame consistently in some situations, but not others, and I can’t figure out what I’m doing wrong.

I’ve simplified my game into a little demo to show the bug. Basic movement of arrow keys + space to jump.

http://www.blobvandam.com/unity/WebPlayer.html

If you jump onto the moving platform just above you when you start, you’ll see the platform works just fine there. No issue. Now move over to the right and jump onto the moving block, and it also works just fine, if you’re stationary. Now keep holding right so the player is pushing against the right edge block of the platform.
Note how when the block is moving right, the collision is just fine, but when the block is moving to the left, the player jitters into the wall.

This is a stripped down version of the much more complete game. The collision bug actually affects my gameplay mechanic in more pronounced ways than this, but it all comes down to this same issue of the raycast apparently lagging behind by one frame.

I use script execution order setting to ensure that the player script runs last, after all of the moving platforms have updated. The platforms run in Update, while the player code runs in LateUpdate (although I have tried every single combination of what runs where to no avail).
The strange part is that if I update the platforms after the player movement, the collision works flawlessly, but obviously with the problem that the platforms are now visibly a frame ahead, because they’ve updated after the player has, but before the rendering. But logically I can’t figure out any reason why this works any different to the buggy version (ignoring the visual difference).

Here’s a quick rundown of how my moving platform code works-
When you land on a block, it stores a reference to the parent in player_parent_transform
At the end of the frame, it stores the local offset of the player from the platform in player_parent_offset
At the start of the frame (which occurs after the platforms have moved), it updates the player position by using the local space offset.

So in theory, moving platforms shouldn’t be an issue, because it’s handled and updated before the player collision occurs. In theory…

Well, hopefully that’s enough info to get the ball rolling. Thanks!

static var script : Player_Movement;

private var input_x : float;
private var input_y : float;

@System.NonSerialized
var player_position : Vector3 = Vector3.zero; //current position
@System.NonSerialized
var player_movement : Vector3 = Vector3.zero; //intended movement for the frame
@System.NonSerialized
var player_speed : Vector3 = Vector3.zero; //intended movement speed in units per second
@System.NonSerialized
var player_groundspeed : float = 0; //player's ground speed
@System.NonSerialized
var player_direction : int = 1; //direction player is facing - 0 = left, 1 = right

private var player_maxspeed : float = 12.0; //player's maximum running speed
private var player_airdrag : float = Mathf.Pow(0.97, 60.0); //air drag
private var player_acc : float = 12.0; //player's acceleration to top running speed
private var player_dec : float = 40.0; //player's deceleration when changing direction
private var player_acc_air : float = 12.0; //player's acceleration in the air
private var player_friction : float = 10.0; //slowing down when not pressing any button (lower for ice)
private var player_jumpvelocity : float = 0.0; //player's jump force
private var player_jumpforce : float = 20; //player's maximum jump force
private var player_gravity : float = 40.0; //player's gravity
private var player_terminalvelocity : float = -20.0; //player's maximum downward speed
private var player_jumpreleased : boolean = false; //state check for releasing jump button for short jump
@System.NonSerialized
var player_air : boolean = false; //whether or not player is in the air

class hitinfo
{
	var hitdistance : float; //length of raycast until it hit the surface
	var hitangle : float; //angle of surface at hit point
	var hit : boolean; //whether or not the ray hit anything
	var raycast : RaycastHit; //stores the raycast info
}

private var player_hitarray : hitinfo[]; 
player_hitarray = new hitinfo[10];

@System.NonSerialized
var player_parent_transform : Transform; //stores the transform for the "parent", ie. on a moving platform
private var player_parent_offset : Vector3 = Vector3.zero; //stores the local offset of the player from the platform inbetween frames

private var player_transform : Transform; //the transform for the player object
//__________________________________________________________________________________________________________________
function Start()
{
	script = gameObject.GetComponent(Player_Movement) as Player_Movement;
	player_transform = transform;

	for (var i : int=0 ; i < 10 ; i++ ) 
	{
		player_hitarray[i] = new hitinfo();
		player_hitarray[i].raycast = new RaycastHit();
	}

	player_position = transform.position; //sets position var to current model position
	
}
//__________________________________________________________________________________________________________________
function LateUpdate()
{

	//grabs inputs
	input_x = Input.GetAxis("Horizontal");
	input_y = Input.GetAxis("Vertical");


//updates player position to reflect new position of moving platforms
	if (player_parent_transform != null)
	{
		player_position = player_parent_transform.TransformPoint(player_parent_offset);
	}
	
		StandardControls(); //handles keyboard input
		NewPosition(); //calculates intended new player position
		WallCollision(); //wall collision. duh
		RoofCollision(); //collision for the top of the player

		if (player_air)
		{
			AirCollision(); //"foot" collision while in the air
		}
		else
		{
			GroundCollision(); //"foot" collision while on the ground
		}

	//positions player object
	player_transform.localPosition = player_position;

	//stores offset to account for moving platforms inbetween frames
	if (player_parent_transform != null)
	{
		player_parent_offset = player_parent_transform.InverseTransformPoint(player_position);
	}
	else
	{
		player_parent_offset = Vector3.zero;
	}


}
//-------------------------------------------------------------------------------------------------------------------------------------------------------------
function GroundCollision() {

var temp_float : float = 0;
var temp_tag : String;
var vector_length : float = (0.9 + 0.35); //second number is maximum step down
var bitmask : int = (1 << 10) + (1 << 11) + (1 << 15) + (1 << 16);

	player_hitarray[0].hit = Physics.Raycast(player_position + Vector3(-0.3,0.9,0), Vector3(0.0,-1.0,0.0), player_hitarray[0].raycast, vector_length, bitmask);
	player_hitarray[1].hit = Physics.Raycast(player_position + Vector3(0,0.9,0), Vector3(0.0,-1.0,0.0), player_hitarray[1].raycast, vector_length, bitmask);
	player_hitarray[2].hit = Physics.Raycast(player_position + Vector3(0.3,0.9,0), Vector3(0.0,-1.0,0.0), player_hitarray[2].raycast, vector_length, bitmask);


	//middle ray
	if (player_hitarray[1].hit)
	{
		player_hitarray[1].hitangle = Mathf.Atan2(player_hitarray[1].raycast.normal.x,player_hitarray[1].raycast.normal.y) * Mathf.Rad2Deg;
	}
	
	//left ray
	if (player_hitarray[0].hit)
	{
		player_hitarray[0].hitangle = Mathf.Atan2(player_hitarray[0].raycast.normal.x,player_hitarray[0].raycast.normal.y) * Mathf.Rad2Deg;
	}
	
	//right ray
	if (player_hitarray[2].hit)
	{
		player_hitarray[2].hitangle = Mathf.Atan2(player_hitarray[2].raycast.normal.x,player_hitarray[2].raycast.normal.y) * Mathf.Rad2Deg;
	}


//calculates ground position
	//middle raycast
	if (player_hitarray[1].hit  Mathf.Abs(player_hitarray[1].hitangle) < 50)
	{
		temp_tag = player_hitarray[1].raycast.transform.tag;
		player_position.y = player_hitarray[1].raycast.point.y;

		if (temp_tag == "Block")
		{
			PlatformAttach(1);
		}
		else
		{
			PlatformDetach();
		}

	}

	//left raycast
	else if (player_hitarray[0].hit  Mathf.Abs(player_hitarray[0].hitangle) < 50)
	{
		temp_tag = player_hitarray[0].raycast.transform.tag;
		temp_float = Mathf.Tan(Mathf.Repeat(player_hitarray[0].hitangle,360.0) * Mathf.Deg2Rad) * 0.3;
		player_position.y = player_hitarray[0].raycast.point.y - temp_float;

		if (temp_tag == "Block")
		{
			PlatformAttach(0);
		}
		else
		{
			PlatformDetach();
		}
	}

	//right raycast
	else if (player_hitarray[2].hit  Mathf.Abs(player_hitarray[2].hitangle) < 50)
	{
		temp_tag = player_hitarray[2].raycast.transform.tag;
		temp_float = Mathf.Tan(Mathf.Repeat(player_hitarray[2].hitangle,360.0) * Mathf.Deg2Rad) * 0.3;
		player_position.y = player_hitarray[2].raycast.point.y + temp_float;
				
		if (temp_tag == "Block")
		{
			PlatformAttach(2);
		}
		else
		{
			PlatformDetach();
		}
	}
	else
	{
		PlatformDetach();
		player_position.y += player_movement.y;	
		player_air = true;
		player_jumpvelocity = 0;
	}

	


}
//-------------------------------------------------------------------------------------------------------------------------------------------------------------
function AirCollision() {
var temp_float : float = 10.0;
var vector_length : float = Mathf.Clamp((0.9 - player_movement.y), 0.9, 10.0);
var bitmask : int = (1 << 10) + (1 << 11) + (1 << 15) + (1 << 16);
var temp_tag : String;


	player_hitarray[0].hit = Physics.Raycast(player_position + Vector3(-0.3,0.9,0), Vector3(0.0,-1.0,0.0), player_hitarray[0].raycast, 4.0, bitmask);
	player_hitarray[1].hit = Physics.Raycast(player_position + Vector3(0,0.9,0), Vector3(0.0,-1.0,0.0), player_hitarray[1].raycast, 4.0, bitmask);
	player_hitarray[2].hit = Physics.Raycast(player_position + Vector3(0.3,0.9,0), Vector3(0.0,-1.0,0.0), player_hitarray[2].raycast, 4.0, bitmask);

	//middle
	if (player_hitarray[1].hit)
	{
		player_hitarray[1].hitangle = Mathf.Atan2(player_hitarray[1].raycast.normal.x,player_hitarray[1].raycast.normal.y) * Mathf.Rad2Deg;
		player_hitarray[1].hitdistance = player_hitarray[1].raycast.distance;
	}
	
	//left
	if (player_hitarray[0].hit)
	{
		player_hitarray[0].hitangle = Mathf.Atan2(player_hitarray[0].raycast.normal.x,player_hitarray[0].raycast.normal.y) * Mathf.Rad2Deg;
		temp_float = Mathf.Tan(Mathf.Repeat(player_hitarray[0].hitangle,360.0) * Mathf.Deg2Rad) * 0.3;
		player_hitarray[0].hitdistance = player_hitarray[0].raycast.distance + temp_float;
	}
	
	//right
	if (player_hitarray[2].hit)
	{
		player_hitarray[2].hitangle = Mathf.Atan2(player_hitarray[2].raycast.normal.x,player_hitarray[2].raycast.normal.y) * Mathf.Rad2Deg;
		temp_float = Mathf.Tan(Mathf.Repeat(player_hitarray[2].hitangle,360.0) * Mathf.Deg2Rad) * 0.3;
		player_hitarray[2].hitdistance = player_hitarray[2].raycast.distance - temp_float;
	}


//checks middle raycast
if (player_hitarray[1].hit  Mathf.Abs(player_hitarray[1].hitangle) < 50  player_hitarray[1].hitdistance <= vector_length  player_jumpvelocity < 19.0)
{
	temp_tag = player_hitarray[1].raycast.transform.tag;
									
	if (temp_tag == "Block")
	{
		player_position.y = player_hitarray[1].raycast.point.y;
		player_air = false;
		PlatformAttach(1);
		return;
	}
	else
	{
		PlatformDetach();
		player_position.y = player_hitarray[1].raycast.point.y;
		player_air = false;
		return;
	}

}

//checks left raycast
if (player_hitarray[0].hit  Mathf.Abs(player_hitarray[0].hitangle) < 50  player_hitarray[0].hitdistance <= vector_length  player_jumpvelocity < 19.0)
{
	temp_tag = player_hitarray[0].raycast.transform.tag;

	if (temp_tag == "Block")
	{
		temp_float = Mathf.Tan(Mathf.Repeat(player_hitarray[0].hitangle,360.0) * Mathf.Deg2Rad) * 0.3;
		player_position.y = player_hitarray[0].raycast.point.y - temp_float;
		player_air = false;
		PlatformAttach(0);
		return;
	}
	else
	{
		PlatformDetach();
		temp_float = Mathf.Tan(Mathf.Repeat(player_hitarray[0].hitangle,360.0) * Mathf.Deg2Rad) * 0.3;
		player_position.y = player_hitarray[0].raycast.point.y - temp_float;
		player_air = false;
		return;
	}
}


//checks right raycast
if (player_hitarray[2].hit  Mathf.Abs(player_hitarray[2].hitangle) < 50  player_hitarray[2].hitdistance <= vector_length  player_jumpvelocity < 19.0)
{
	temp_tag = player_hitarray[2].raycast.transform.tag;

	if (temp_tag == "Block")
	{
		temp_float = Mathf.Tan(Mathf.Repeat(player_hitarray[2].hitangle,360.0) * Mathf.Deg2Rad) * 0.3;
		player_position.y = player_hitarray[2].raycast.point.y + temp_float;
		player_air = false;
		PlatformAttach(2);
		return;
	}
	else
	{
		PlatformDetach();
		temp_float = Mathf.Tan(Mathf.Repeat(player_hitarray[2].hitangle,360.0) * Mathf.Deg2Rad) * 0.3;
		player_position.y = player_hitarray[2].raycast.point.y + temp_float;
		player_air = false;		
		return;
	}
}



if (player_movement.y < 0)
{
	player_position.y += player_movement.y;
	
}



}
//-------------------------------------------------------------------------------------------------------------------------------------------------------------
function RoofCollision() {
var temp_float : float = 10.0;
var temp_int :int = 0;
var i : int = 0;
var vector_length : float = 0.0;
var temp_tag : String;
var bitmask : int = (1 << 10) + (1 << 15);

		vector_length = Mathf.Clamp((0.9 + player_movement.y), 0.9, 10.0);
		player_hitarray[1].hit = Physics.Raycast(player_position + Vector3(-0.3, 0.9, 0), Vector3(0.0, 1.0, 0.0), player_hitarray[1].raycast, vector_length, bitmask);
		player_hitarray[2].hit = Physics.Raycast(player_position + Vector3(0.0, 0.9, 0), Vector3(0.0, 1.0, 0.0), player_hitarray[2].raycast, vector_length, bitmask);
		player_hitarray[3].hit = Physics.Raycast(player_position + Vector3(0.3, 0.9, 0), Vector3(0.0, 1.0, 0.0), player_hitarray[3].raycast, vector_length, bitmask);
	
		for (i = 1 ; i < 4 ; i++ ) {
			if (player_hitarray[i].hit) {
				player_hitarray[i].hitangle = Vector3.Angle(Vector3(0,1,0), player_hitarray[i].raycast.normal);
				if (player_hitarray[i].hitangle > 135.0  player_hitarray[i].hitangle < 225.0) {
					if (player_hitarray[i].raycast.distance < temp_float)
					{
						temp_float = player_hitarray[i].raycast.distance;
						temp_int = i;
					}
				}
			}
		}

		if (temp_int != 0)
		{
			if (player_jumpvelocity > 0.0)
			{
				player_jumpvelocity = -1.0; //if you hit your head while jumping upwards, it gives you a very slight downward speed
			}
			else
			{
				player_jumpvelocity = -10.0; //if you hit your head while already travelling downward, you're probably being hit by something also travelling downward, so it gives you a faster speed to compensate
			}
			
			player_position.y += (temp_float - 0.95);
			temp_tag = player_hitarray[temp_int].raycast.transform.tag;
		}
		else
		{
			if (player_air  player_movement.y > 0) player_position.y += player_movement.y;
		}
	
}
//-------------------------------------------------------------------------------------------------------------------------------------------------------------
function WallCollision() {
var temp_float : float = 1.0;
var temp_int : int = 0;
var temp_tag : String;
var i : int = 0;
var vector_length : float = 0;
var vector_direction : Vector3;
var bitmask : int = (1 << 10) + (1 << 15) + (1 << 16);



if (player_movement.x < 0)
{
	vector_direction = Vector3(-1.0,0,0); //casts rays while traveling left
}
else
{
	vector_direction = Vector3(1.0,0,0); //casts rays while traveling right
}


	vector_length = (0.35 + Mathf.Abs(player_movement.x));
	Debug.Log(vector_length);
	if (player_air  (player_speed.y > 0))
	{
		player_hitarray[1].hit = Physics.Raycast(player_position + Vector3(0.0, 0.01, 0), vector_direction, player_hitarray[1].raycast, vector_length, bitmask);
	}
	else
	{
		player_hitarray[1].hit = Physics.Raycast(player_position + Vector3(0.0, 0.2, 0), vector_direction, player_hitarray[1].raycast, vector_length, bitmask);	
	}
	player_hitarray[2].hit = Physics.Raycast(player_position + Vector3(0.0, 0.45, 0), vector_direction, player_hitarray[2].raycast, vector_length, bitmask);
	player_hitarray[3].hit = Physics.Raycast(player_position + Vector3(0.0, 0.9, 0), vector_direction, player_hitarray[3].raycast, vector_length, bitmask);
	player_hitarray[4].hit = Physics.Raycast(player_position + Vector3(0.0, 1.35, 0), vector_direction, player_hitarray[4].raycast, vector_length, bitmask);
	player_hitarray[5].hit = Physics.Raycast(player_position + Vector3(0.0, 1.75, 0), vector_direction, player_hitarray[5].raycast, vector_length, bitmask);

	temp_float = 1.0;
	temp_int = 0;
	for (i = 1 ; i < 6 ; i++)
	{
	
		if (player_hitarray[i].hit)
		{
			player_hitarray[i].hitangle = Vector3.Angle(Vector3(0,1,0), player_hitarray[i].raycast.normal);
			if (player_hitarray[i].hitangle > 50.0  player_hitarray[i].hitangle <= 135.0)
			{
				if (player_hitarray[i].raycast.distance < temp_float)
				{
					temp_float = player_hitarray[i].raycast.distance;
					temp_int = i;
				}
			}
		}
	}

	if (temp_int != 0)
	{
		temp_tag = player_hitarray[temp_int].raycast.transform.tag;
		player_groundspeed = 0.0;
		player_position.x = player_hitarray[temp_int].raycast.point.x - (0.35 * vector_direction.x);
	}
	else
	{
		player_position.x += player_movement.x;
	}
}
//-------------------------------------------------------------------------------------------------------------------------------------------------------------
function NewPosition()
{

if (!player_air) //calculates player movement while player is on the ground
{ 
	player_speed.x = player_groundspeed;
	player_speed.y = 0;
	
	player_movement = player_speed * Time.deltaTime; //converts player speed to movement for this frame
}
else //calculates new player position while in the air
{
		PlatformDetach();
		player_jumpvelocity -= (player_gravity * Time.deltaTime);
		if (player_jumpvelocity < player_terminalvelocity) { player_jumpvelocity = player_terminalvelocity; }
		player_speed.x = player_groundspeed;
		player_speed.y = player_jumpvelocity;
		
		player_movement = player_speed * Time.deltaTime;
	
}


}
//-------------------------------------------------------------------------------------------------------------------------------------------------------------
function StandardControls()
{

//REGULAR CONTROLS
	//ground controls
	if (!player_air) 
	{ 
		player_jumpvelocity = 0;
	
	
	//JUMP BUTTON
		if (Input.GetButtonDown("Jump"))
		{
			player_jumpreleased = false; //resets jump button release (for short jumps)
			player_jumpvelocity = player_jumpforce; //sets initial jump velocity
			player_air = true; //sets player flag to in the air
		}
	
	
	//PLAYER PRESSING RIGHT
		if (input_x > 0)
		{
			if (player_groundspeed < 0)
			{
				player_groundspeed = Mathf.MoveTowards(player_groundspeed, player_maxspeed * input_x, (player_dec * Time.deltaTime));
			}
			else
			{
				player_groundspeed = Mathf.MoveTowards(player_groundspeed, player_maxspeed * input_x, (player_acc * Time.deltaTime));
				player_direction = 1;
			}
		}
		
	//PLAYER PRESSING LEFT
		else if (input_x < 0)
		{
			if (player_groundspeed > 0)
			{
				player_groundspeed = Mathf.MoveTowards(player_groundspeed, player_maxspeed * input_x, (player_dec * Time.deltaTime));
			}
			else
			{
				player_groundspeed = Mathf.MoveTowards(player_groundspeed, player_maxspeed * input_x, (player_acc * Time.deltaTime));
				player_direction = 0;
			}
		}
			
	//PLAYER NOT PRESSING LEFT OR RIGHT
		else
		{
			player_groundspeed = Mathf.MoveTowards(player_groundspeed, 0, (player_friction * Time.deltaTime));
		}
		

		player_groundspeed = Mathf.Clamp(player_groundspeed, -player_maxspeed, player_maxspeed);

	}
	else
	{ //air controls
		if (input_x > 0)
		{
			player_groundspeed = Mathf.MoveTowards(player_groundspeed, player_maxspeed * input_x, (player_acc_air * Time.deltaTime));
			player_direction = 1;
		}
		
		else if (input_x < 0)
		{
			player_groundspeed = Mathf.MoveTowards(player_groundspeed, player_maxspeed * input_x, (player_acc_air * Time.deltaTime));
			player_direction = 0;
		}
		
		else
		{
			player_groundspeed *= Mathf.Pow(player_airdrag, Time.deltaTime);
		}
		
		player_groundspeed = Mathf.Clamp(player_groundspeed, -player_maxspeed, player_maxspeed);

	
		if (!Input.GetButton("Jump")) //releasing jump button for short jump
		{
			if (player_jumpvelocity > 10.0  !player_jumpreleased)
			{
				player_jumpvelocity = 10.0;
				player_jumpreleased = true;
			}
		}
	}



}
//-------------------------------------------------------------------------------------------------------------------------------------------------------------
function PlatformAttach(index : int)
{
	//sets new "parent", for when player is on moving platform
	player_parent_transform = player_hitarray[index].raycast.transform;
	
}
//-------------------------------------------------------------------------------------------------------------------------------------------------------------
function PlatformDetach()
{
	//sets player back to world space
	player_parent_transform = null;

}
//-------------------------------------------------------------------------------------------------------------------------------------------------------------

Bump. I still can’t figure this out after almost 2 weeks. This is soooo frustrating. I could post the whole project if someone would be able to help out.

The raycasts aren’t registering any collision until the player is well into the wall, at which point it will detect the wall and put the player back to where he should be. I can’t for the life of me figure out why it’s missing the raycast though.

If you move your colliders you will have to wait for the next FixedUpdate to be called before the raycasts can find them at their new positions. So make your raycasting in FixedUpdate and you should be fine

My platforms currently aren’t moving using physics. They’re all being moved during the regular Update function using transform.position, so they should update at the same rate as my player collision.
Would they still only update during FixedUpdate? The raycasting for the feet updates properly and keeps the character perfectly on the platform, so it appears the platforms update their position correctly before my character movement.

Yes, the colliders are actually moved only by the physics engine in fixedupdate. Otherwise it would have to iterate every time you changed the position of a collider. Raycasting for the feet probably works because the platform is big enough and slow enough so it doesnt move away from under the feet in a single fixed time step.

Thanks for the great answer!
I didn’t expect that everything would update right when I gave the command, but I assumed that since I gave the command in Update that it would probably move it at the end of the Update cycle.
It seems that was a faulty assumption, which would explain why I haven’t been able to track down my problem. I’ll try FixedUpdate and see if it helps!