1
\$\begingroup\$

I have just started working with Unity to simply learn how it works. I've been trying to get grid-based movement and collision with a tilemap working, and it's mostly there, but there's one bit that's not quite right and I'm unsure as to why.

Currently, the player sprite is moved in a grid-based fashion. When it approaches an obstacle, the movement script uses Physics.OverlapCircle, filtering on the collision layer, to determine whether the player can advance to the next grid cell. This works, save for the fact that it appears to be using the entire bounds of the sprite for collision detection, whereas I want to allow the top portion of the sprite (i.e. the head) to overlap and actually detect collision with the lower portion (the body).

Here is a screenshot of my editor, with the player selected. You can see that the player appears to be covering a 2x2 area of the grid (I think this has to do with the size of the sprite not matching the tilemap). The player sprite is 32x32 with Pixels Per Unit defined as 32, whereas the tilemap has a Pixels Per Unit defined as 16.

Unity Editor Window with Player and Grid selected

Here is my movement script, as well. The method at the bottom of the file, CanMoveToTargetPosition covers the overlap logic.

using System.Collections.Generic;
using UnityEngine;

public class Movement : MonoBehaviour
{
    private const float movementSpeed = 5f;

    public Transform movementTarget;
    public LayerMask collisionLayer;
    public Animator animator;

    // Start is called before the first frame update
    void Start()
    {
        movementTarget.parent = null;
    }

    // Update is called once per frame
    void Update()
    {
        MovePlayer();

        if (ShouldCheckMovement())
        {
            CheckInputForMovement();
        }
    }

    private void MovePlayer()
    {
        transform.position = Vector3.MoveTowards(transform.position, movementTarget.position, movementSpeed * Time.deltaTime);
    }

    private bool ShouldCheckMovement() => Vector3.Distance(transform.position, movementTarget.position) <= .05f;

    private void CheckInputForMovement()
    {
        float horizontal = Input.GetAxisRaw("Horizontal");
        float vertical = Input.GetAxisRaw("Vertical");

        float horizontalPositive = Mathf.Abs(horizontal);
        float verticalPositive = Mathf.Abs(vertical);

        var velocity = Vector3.zero;
        if (horizontalPositive == 1f)
        {
            velocity = new Vector3(horizontal, 0f);
        }
        else if (verticalPositive == 1f)
        {
            velocity = new Vector3(0f, vertical);
        }

        var targetPosition = movementTarget.position + velocity;
        if (!CanMoveToTargetPosition(targetPosition))
        {
            movementTarget.position = targetPosition;
        }

        animator.SetFloat("Horizontal", horizontal);
        animator.SetFloat("Vertical", vertical);

        float speed = Mathf.Clamp((horizontalPositive + verticalPositive) / 2f, 0f, 1f);
        animator.SetFloat("Speed", speed);
    }

    private bool CanMoveToTargetPosition(Vector2 targetPosition) =>
        Physics2D.OverlapCircle(targetPosition, .2f, collisionLayer);
}

The tilemap has an attached TilemapCollider2D component. I've tried including a RigidBody2D with a CompositeCollider2D, but this actually makes the result worse - the player collides with some parts of the collision layer, but can walk through others.

I've tried using a BoxCollider2D to allow me to define the actual collision area on the player, but I don't seem to be able to then get the overlap collision working with the tilemap.

Here is a view of my collision layer with the tilemap collision areas defined:

Collision Layer

Finally, you can see a recording of my issue as a GIF or MP4.

I'm sure this is more to do with my lack of understanding of how the physics overlap, or colliders work in Unity, but I'm struggling to find any resources to help with this specific issue. So, any links for further reading would be appreciated! I did manage to get non-grid-based movement working with collision detection by just using a Rigidbody2D and BoxCollider2D on the player, and TilemapCollider2D, RigidBody2D and CompositeCollider2D on the tilemap. But I would like to retain the grid-based movement.

Any help would be appreciated! Thanks.

\$\endgroup\$

4 Answers 4

1
\$\begingroup\$

In Unity, you usually don't need to check if the movement destination is free, because collision detection and resolution can be handled by the engine. When you use rigidbody movement, then the engine will do so automatically and just not move the rigidbody when there is an obstacle in the way. Or if that object has a dynamic rigidbody, it will push it out of the way in a physically correct manner.

But in order to do that, you actually have to use the Rigidbody / Rigidbody2D component for movement. When you use transform.position = ... then you are not moving the object, you are teleporting the object. Which means the engine will be perfectly fine with the object getting teleported through a wall or even into a wall.

If you want to move an object in a physical way, use GetComponent<Rigidbody2D>().MovePosition(newPosition). This will move the object towards the position, and stop when there is another solid collider (with a static or kinematic rigidbody or a rigidbody-less collider with the "static" flag set on the gameObject) in the way. Colliders with dynamic rigidbodies will get pushed out of the way instead, but won't slow the player down.

If you want the player to be able to interact with other dynamic rigidbodies in a more physically realistic way, use AddForce instead of MovePosition to control its movement. Using forces instead of moving also allows you to make the object retain its momentum, which can sometimes result in a better game experience. But in most cases you might want to increase the linear drag of the rigidbody, as it otherwise often feels far too slippery (the default is intended for objects flying through the air, not for objects moving on a solid surface).

Here is a little demo which shows how different ways to implement movement work in Unity and how they affect gameplay.

\$\endgroup\$
4
  • \$\begingroup\$ It sounds like this could have worked, as my logic would have tried to move to the next tile, but would have been physically blocked by the engine. However, I suppose it could have a problem if the tilemap collider's bounds didn't exactly align to the grid (in that the player could end up moving slightly closer to the next tile). \$\endgroup\$ Commented Jan 17, 2022 at 15:16
  • \$\begingroup\$ Another issue I have found with this, that I did actually encounter when using transform.position = ... (I assume it will still occur with the MovePosition you suggested), is: because I'm setting the target position as the next tile, then my logic checks on each frame update if I've reached that target yet. If not, it continues to try to move and doesn't check for further input until I'm closer (as I don't want to allow direction switching mid-move). Following your answer, the player would get stuck constantly walking into a wall. Any suggestions to get around that? \$\endgroup\$ Commented Jan 19, 2022 at 10:42
  • \$\begingroup\$ @SamuelSlade Do you seriously want tile-by-tile movement? Unity is much better at handling movement in an analog manner. But if you insist, then I would recommend to not deal with Collider2Ds and Physics2D at all and just query the Tilemap to see if there is a blocking tile at the destination. Want me to write another answer to explain the details? \$\endgroup\$
    – Philipp
    Commented Jan 19, 2022 at 11:39
  • \$\begingroup\$ It was more for experimentation and learning - recreating the behaviour from the original Pokemon games. I'm working on another project without tile-based movement, but it would be good to understand the mechanics properly here. If you could provide a second answer specific to the tile-based movement and the best way to approach that, then that would be appreciated. I did manage to solve it using this approach gamedev.stackexchange.com/a/198809/8792 but that might not be the best way, so I'm open to alternatives \$\endgroup\$ Commented Jan 24, 2022 at 10:28
0
\$\begingroup\$

First, a design question: Why. Why do you need physics? You could simply ask if the TileMap contains a certain tile that is not walkable, for example, and then, when you walk, check your character to see if he walks into that tile. The colliders seem overkill imho.

Or the other way around: why do you have the tilemap? If these were just prefabs with sprites, you could easily edit their colliders, and probably save you a lot of colliders.

From the video I would conclude that you want your character to be able to walk closer to objects and even behind the fence. So the fence collider should be lower and maybe edit your character to have a smaller collider for walking sake, like a small circle at the base where the feet are, so that his entire upper body can overlap what is behind him.

I suspect that none of your tiles really need a rigidbody component. Let the player do all the collision checks - it is not necessary for both game objects to have a rigidbody. They just need both colliders - if that's the way you want to do it anyway.

\$\endgroup\$
1
  • \$\begingroup\$ Thanks for your response. In answer to your questions: I am using the tilemap as a way to paint the map, rather than adding sprites individually, though I'm open to suggestions on other ways of doing this. As for physics, this was just one approach I followed from an example, that worked without grid-based movement. In my grid-based movement approach, I just use the TilemapCollider2D and do the check to see if the player can move, but that is where the problem lies, because I can't seem to control how much of the player is taken into account in that collision check \$\endgroup\$ Commented Jan 11, 2022 at 10:24
0
\$\begingroup\$

I have managed to find an answer, though I am still open to any better answers.

By adding a child object to the player, I was able to position it (by initially adding a square sprite renderer with white colour, so I could see it). I positioned this child object in the area I wanted to collide. I changed my collision detection to be based on the position of this child but still moved the parent object as before.

Here is the updated example, where colliderObject is this child object.

using UnityEngine;

public class Movement : MonoBehaviour
{
    private const float movementSpeed = 5f;

    public Transform movementTarget;
    public LayerMask collisionLayer;
    public GameObject colliderObject;
    public Animator animator;

    // Start is called before the first frame update
    void Start()
    {
        movementTarget.parent = null;
    }

    // Update is called once per frame
    void Update()
    {
        MovePlayer();

        if (ShouldCheckMovement())
        {
            CheckInputForMovement();
        }
    }

    private void MovePlayer()
    {
        transform.position = Vector3.MoveTowards(transform.position, movementTarget.position, movementSpeed * Time.deltaTime);
    }

    private bool ShouldCheckMovement() => Vector3.Distance(transform.position, movementTarget.position) <= .05f;

    private void CheckInputForMovement()
    {
        float horizontal = Input.GetAxisRaw("Horizontal");
        float vertical = Input.GetAxisRaw("Vertical");

        float horizontalPositive = Mathf.Abs(horizontal);
        float verticalPositive = Mathf.Abs(vertical);

        var velocity = Vector3.zero;
        if (horizontalPositive == 1f)
        {
            velocity = new Vector3(horizontal, 0f);
        }
        else if (verticalPositive == 1f)
        {
            velocity = new Vector3(0f, vertical);
        }

        var collisionTarget = colliderObject.transform.position + velocity;
        if (!CanMoveToTargetPosition(collisionTarget))
        {
            movementTarget.position = transform.position + velocity;
        }

        animator.SetFloat("Horizontal", horizontal);
        animator.SetFloat("Vertical", vertical);

        float speed = Mathf.Clamp((horizontalPositive + verticalPositive) / 2f, 0f, 1f);
        animator.SetFloat("Speed", speed);
    }

    private bool CanMoveToTargetPosition(Vector2 targetPosition) =>
        Physics2D.OverlapCircle(targetPosition, .2f, collisionLayer);
}

Here is a video of the solution in action:

I will keep this answer as unaccepted for now, in case anyone has any better answers.

\$\endgroup\$
0
\$\begingroup\$

Attach Collider2D, RigidBody2D, CharacterController2D to player and use Move Method in FixUpdate. Meanwhile, properties of RigidBody2D: Body Type = Dynamic, Simulated = true, Gravity Scale = 0, Constraints - Freeze Rotation Z = true

using UnityEngine;

[RequireComponent(typeof(Rigidbody2D))]
public class CharacterController2D : MonoBehaviour
{
    #region Move Var

    [SerializeField, Tooltip("Max speed, in units per second, that the character moves.")]
    float speed = 2;

    [SerializeField, Tooltip("Acceleration while grounded.")]
    float walkAcceleration = 20f;

    [SerializeField, Tooltip("Deacceleration while stopped.")]
    float deceleration = 80f;

    [SerializeField, Tooltip("Stop when move input smaller than stopThreshold")]
    float stopThreshold = 0.1f;

    private Rigidbody2D rg;

    private Vector2 velocity;

    public bool isMoving
    {
        get { return velocity != Vector2.zero; }
    }

    #endregion

    private void Awake()
    {
        rg = GetComponent<Rigidbody2D>();
    }

    public void Move(Vector2 moveInput)
    {
        // Move Logic

        if (moveInput.magnitude >= stopThreshold)
            velocity = Vector2.MoveTowards(velocity, speed * moveInput, walkAcceleration * Time.fixedDeltaTime);
        else
            velocity = Vector2.MoveTowards(velocity, Vector2.zero, deceleration * Time.fixedDeltaTime);

        if (velocity.magnitude <= stopThreshold)
            velocity = Vector2.zero;

        Vector2 curPos = new Vector2(transform.position.x, transform.position.y);
        Vector2 nextPos = curPos + velocity * Time.fixedDeltaTime;

        rg.MovePosition(nextPos);
}
\$\endgroup\$

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .