What I want to achieve:
When moving the player it moves in steps of 1, thus in tiles. When walking against boxes it can move those boxes as long as there is no collider in the direction in which you want to move. (Right now there is no limit on how many boxes you can push). When a box (or player) is pushed into an air zone, it will move in the same direction of the air zone till they are out of the air zone. If they collide with boxes at the end of the air zone, they will push those boxes.
What I have so far:
So far the air zone is working, it will push boxes into the desired direction and stop moving once they reached the end. The player can also push all the boxes and cant push those anymore once they hit a wall.
The problem:
However, the problem mostly lays with collision detection. When pushing boxes, there is a probability to not push the box anymore and phase through the box as a player. I don't know how to fix this issue.
Things I tried:
I've tried several things, by which I don't even know what I have tried any more.
Some gifs:
Corner detection when wanting to be on the same spot at the same time (works... good enough)
Pushing multiple boxes, sometimes phasing through (not working at all)
Main code:
public abstract class Movement : MonoBehaviour
{
//=========================================================================================
// > Variables <
//=========================================================================================
//------------------------ public ------------------------
public Vector3 toBePosition; //position it will be in after lerp
public Vector3 leftDirection;
public Vector3 rightDirection;
//----------------------- private ------------------------
private int terrainLayer = 6;
private int movableLayer = 7;
protected Vector3 _currentPosition;
private Vector3 _targetPosition;
protected float _travelTime = 0.1f;
private float timer;
public bool canMove = true;
//=========================================================================================
// > Start/Update <
//=========================================================================================
protected virtual void Start()
{
toBePosition = transform.position;
_currentPosition = transform.position;
_targetPosition = transform.position;
}
protected virtual void Update()
{
//lerp position
timer += Time.deltaTime;
float ratio = timer / _travelTime;
transform.position = Vector3.Lerp(_currentPosition, _targetPosition, ratio);
//gravity
checkForFalling();
//inputs
checkForMovement();
}
public void checkForFalling()
{
if (canMove)
{
if (Physics.Raycast(_currentPosition, -Vector3.up, out RaycastHit hit, 1f))
{
//if we hit something that isnt a airchannel nor a terrain, it will move down.
if (hit.collider.gameObject.tag != "AirChannel" && hit.collider.gameObject.layer != terrainLayer)
{
moveToTile(-Vector3.up);
}
}
//didnt detect anything, thus we need to fall
else
{
moveToTile(-Vector3.up);
}
}
}
//=========================================================================================
// > Public Tool Functions <
//=========================================================================================
public void moveToTile(Vector3 pDirection)
{
if (canMove)
{
//get normalized direction just makes sure the direction on the xyz is always either 0 or 1. (sometimes it would be 0.0000001)
pDirection = getNormalizedDirection(pDirection);
//if there isnt a wall update our target position to where we want to go.
if (!wallCheck(_currentPosition + pDirection, _currentPosition))
{
_targetPosition = pDirection + _currentPosition;
toBePosition = _targetPosition;
timer = 0f;
}
}
}
protected void checkForMovement()
{
//makes sure we dont move multiple tiles within the same amount of time
//because when holding the button id would stack if we wouldnt do this.
if ((_targetPosition - transform.position).magnitude < 0.001f)
{
canMove = true;
transform.position = _targetPosition;
_currentPosition = transform.position;
}
else
{
canMove = false;
}
}
//=========================================================================================
// > Private Tool Functions <
//=========================================================================================
virtual public bool wallCheck(Vector3 pTargetPosition, Vector3 pCurrentPosition)
{
//get the direction and make sure they are either 0 or 1 again.
Vector3 moveDirection = (pTargetPosition - pCurrentPosition).normalized;
moveDirection = getNormalizedDirection(moveDirection);
//calculate the left and right tile of the forward tile.
leftDirection = getLeftFromDirection(moveDirection);
rightDirection = getRightFromDirection(moveDirection);
//debug rays to visualize the raycasts.
Debug.DrawRay(pCurrentPosition - moveDirection * 0.1f, moveDirection * 1.4f, Color.green , 5);
Debug.DrawRay(pCurrentPosition - moveDirection * 0.1f, leftDirection * 1.4f, Color.green , 5);
Debug.DrawRay(pCurrentPosition - moveDirection * 0.1f, rightDirection * 1.4f, Color.green, 5);
//============================== Collision Checks ===================================
//first we check right in front of us.
if (Physics.Raycast(pCurrentPosition, moveDirection, out RaycastHit frontHit, 1.4f))
{
//if we hit terrain we return true because it means we hit a wall
if (frontHit.collider.gameObject.layer == terrainLayer)
{
return true;
}
//if we hit a something on a movable layer, we will start a recursive loop to check if there is a empty spot to move into.
if (frontHit.collider.gameObject.layer == movableLayer)
{
return frontHit.collider.gameObject.GetComponent<Movement>().wallCheck(pTargetPosition + moveDirection, pTargetPosition);
}
else { return false; }
}
//now we check on the left side (for the corner collision)
if (Physics.Raycast(pCurrentPosition, leftDirection, out RaycastHit leftHit, 1.45f))
{
if (leftHit.collider.gameObject.layer == movableLayer)
{
//if we hit a movable layer and its also moving into the same position as we are, then move us back 1 tile.
//even though i return true, and the code should stop and not move anymore, this would still happen, thus needed to move 1 back
//a fix for this would be appreciated.
if (pTargetPosition == leftHit.collider.gameObject.GetComponent<Movement>().toBePosition)
{
moveToTile(moveDirection *= -1f);
return true;
}
else { return false; }
}
else { return false; }
}
//We do the same for the right side as we did with the left side.
if (Physics.Raycast(pCurrentPosition, rightDirection, out RaycastHit rightHit, 1.45f))
{
if (rightHit.collider.gameObject.layer == movableLayer)
{
if (pTargetPosition == rightHit.collider.gameObject.GetComponent<Movement>().toBePosition)
{
moveToTile(moveDirection *= -1f);
return true;
}
else { return false; }
}
else { return false; }
}
else { return false; }
}
protected Vector3 getNormalizedDirection(Vector3 oldDirection)
{
//makes sure everything is either 0 or 1.
Vector3 newDirection = oldDirection;
if (newDirection.x > 0.1f)
{
newDirection.x = 1;
}
else if (newDirection.x < -0.1f)
{
newDirection.x = -1;
}
else
{
newDirection.x = 0;
}
if (newDirection.z > 0.1f)
{
newDirection.z = 1;
}
else if (newDirection.z < -0.1f)
{
newDirection.z = -1;
}
else
{
newDirection.z = 0;
}
return newDirection;
}
private Vector3 getLeftFromDirection(Vector3 pDirection)
{
//calulcates the tile on the left side from given direction
Vector3 left = pDirection;
if(left.x == 0)
{
left.x -= 1;
}
if(left.z == 0)
{
left.z -= 1;
}
return left;
}
private Vector3 getRightFromDirection(Vector3 pDirection)
{
//calculates the tile on the right side from the given direction.
Vector3 left = pDirection;
if (left.x == 0)
{
left.x += 1;
}
if (left.z == 0)
{
left.z += 1;
}
return left;
}
Player Movement code:
public class PlayerMovement : Movement
{
[SerializeField] private float _moveSpeed;
private InputManager _inputManager;
public int playerPushWeight;
private void Awake()
{
serviceLocator.AddToList("Player1", this.gameObject);
}
protected override void Start()
{
base.Start();
_inputManager = serviceLocator.GetFromList("InputManager").GetComponent<InputManager>();
}
// Update is called once per frame
protected override void Update()
{
base.Update();
if (_inputManager.GetAction(InputManager.Action.HORIZONTAL))
{
moveToTile(new Vector3(_inputManager.getHorizontalInput(),0,0));
}
if (_inputManager.GetAction(InputManager.Action.VERTICAL))
{
moveToTile(new Vector3(0, 0, _inputManager.getVerticalInput()));
}
//this allows other objects to move this object. (Boxes now can move the player, useful for airchannels)
if (wallCheckCalled)
{
moveToTile(_direction);
}
wallCheckCalled = false;
}
private Vector3 _direction;
private bool wallCheckCalled;
override public bool wallCheck(Vector3 pTargetPosition, Vector3 pCurrentPosition)
{
bool isWall = base.wallCheck(pTargetPosition, pCurrentPosition);
if (!isWall)
{
wallCheckCalled = true;
_direction = pTargetPosition - pCurrentPosition;
}
return isWall;
}
Box movement:
public class WeightMovement : Movement
{
//=========================================================================================
// > Variables <
//=========================================================================================
//------------------------ public ------------------------
//----------------------- private ------------------------
private Vector3 _direction;
private bool wallCheckCalled;
//=========================================================================================
// > Start/Update <
//=========================================================================================
//=========================================================================================
// > Public Tool Functions <
//=========================================================================================
override public bool wallCheck(Vector3 pTargetPosition, Vector3 pCurrentPosition)
{
bool isWall = base.wallCheck(pTargetPosition, pCurrentPosition);
if (!isWall)
{
wallCheckCalled = true;
_direction = pTargetPosition - pCurrentPosition;
}
return isWall;
}
protected override void Update()
{
if (wallCheckCalled)
{
moveToTile(_direction);
}
base.Update();
wallCheckCalled = false;
}