1

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)

Pushing boxes into air zone, sometimes phasing through. (not working, but previous one is more important)

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;
    }
Lilly
  • 61
  • 3
  • Have you considered grid-based collisions instead of relying on raycasts? That could be much easier, if it's an option for you. – hugo May 17 '21 at 13:54
  • That I would make an array of all available positions things can stand in, and move them according to that? And check if something is already stored on that position? Should be possible, but its still a 3D space, there will be gravity and all. Not sure if I like the idea of such a big array. – Lilly May 17 '21 at 13:59
  • If objects are not _logically_ constrained to the grid (because of gravity or other things), then maybe what I suggested is not an option at all. However if the concern is the map being truly huge in relation with the number of objects, you could use a more compact structure than a sparse array. – hugo May 17 '21 at 14:05
  • Well even with gravity it would still be constrained to the grid, but I don't know how big the map is going to be, and each level is going to be different. Thats why I kinda tried to find a solution around the array, I can still try to do it with an array though. – Lilly May 17 '21 at 14:07
  • There could be more suited data structures, but even a `Dictionary` would do well for an arbitrarily-sized map. I say in any case that data will be useful to analyse the state of the board (for instance if you need to check if the player has aligned 4 crates) – hugo May 17 '21 at 14:17
  • @Lilly how BIG would the array be ??? how many along each side ? – Fattie May 17 '21 at 17:08
  • @Lilly - you gotta be joking, looking at your images, there might be "30 or 40" units along each side. so long as your array size is less than *let's say ten million* it's an absolute non-issue. your sense of scale of what computers can do is incredibly off :) PhysX is doing an INCREDIBLE amount of spatial hashing calculations to achieve what's happening in your gifs. it's very like you want to do this with numbers, not with PhysX. enjoy – Fattie May 17 '21 at 17:11
  • that being said, you should be easily able to fix the PhysX issue. Do you simply have to reduce the penetration setting or such ? – Fattie May 17 '21 at 17:12

1 Answers1

0

I am not using tile maps for this, but use sprites instead.

The System

You should use a raycasting like system. Basically, you would raycast and detect the objects in front of the player with a distance of one. Then you should do the same thing again. If it hits another block, (in the same direction) then it will do same thing over again. Once it hits either a wall or nothing, it will start to move. If it hits a wall, it won’t.

The Movement

Because we wanted a movement system where all transforms have no decimals. I will do this by moving it by one every time.


The Player Script

using System;
using System.Collections.Generic;
using UnityEngine;

...

RaycastHit hit;
List<Transform> blocks;


void Update()
{
   if (Input.GetKeyDown(KeyCode.W))
   {
      Move(Vector3.forward);
   }
   else if (Input.GetKeyDown(KeyCode.A))
   {
      Move(-Vector3.right);
   }
   else if (Input.GetKeyDown(KeyCode.S))
   {
      Move(-Vector3.forward);
   }
   else if (Input.GetKeyDown(KeyCode.D))
   {
      Move(Vector3.right);
   }
}
void Move(Vector3 direction)
{
   blocks.Clear();
   if (Raycheck(direction))
   {
      Debug.Log(“The player has moved because there was no objects in front of them.”);

      for (int i = 0; i < blocks.Length; i++)
      {
         blocks[i].Translate(direction);
      }
      transform.Translate(direction);
   }
   else
   {
      Debug.Log(“There are objects in the player’s way, and they are trying to move.”);
   }
}
public static void Raycheck(Vector3 direction)
{
   if (Physics.Raycast(transform.position, direction, out hit, 1)
   {
      string tag = hit.collider.gameObject.tag;
      if (tag == “block”)
      {
         Debug.Log(“Raycasting: hit a block”);

         blocks.Add(hit.collider.gameObject.transform);
         return Raycheck(direction);
      }
      if (tag == “wall”)
      {
         Debug.Log(“Raycasting: hit a wall”);
         return false;
      }
   }
   else
   {
      Debug.Log(“Raycasting: finished. Hit the air”);
      return true;
   }
}

Here, we get the input (update function), and call the move function inputting the direction they moved. We detect the blocks in front of the player by raycasting (raycheck function). It adds the blocks that are in the players way to a list. The function will return true if there is air at the end of the line of blocks, and false if there is wall at the end. With this list of blocks, it will move it in the direction that they pressed.


This wasn’t tested and may be very buggy. I will be able to test this tonight or tomorrow night. If you get any simple errors (like missing a semicolon) do your best to figure it out. If you figured them out or not, please list the errors in the comments of this post.

I will try and fix it either tonight or tomorrow night depending on how much work I have.

gbe
  • 1,003
  • 1
  • 7
  • 23