Unity2D Move character in tile increments with collision

This is a case where I feel Unity is simply too modern to handle much of any kind of antiquated player controller setup.

What I want is extremely simple and that is the same player controller as can be found on most Gameboy games. The player sits on a tile and holding down a directional button causes the player to smoothly advance tile-by-tile unless it collides with something in which case often the player will keep walking but will never leave the tile because theirs a colliding tile in the way.

Unity seems geared for modern games which are more complex and can take advantage of full-on physics with velocity and everything so it’s a complete nightmare to make this work and I’ve exhausted all options imaginable.

I have the player controlelr setup without collisions and it works really great

``````using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Tilemaps;

public class InputMovement : MonoBehaviour
{
public float speed = 60f;
private Coroutine anim = null;
private float fraction = 0;

void Update()
{
if (Input.GetKey(KeyCode.W))
{
anim = StartCoroutine(SetWalkUp());
}

else if (Input.GetKey(KeyCode.A))
{
anim = StartCoroutine(SetWalkLeft());
}

else if (Input.GetKey(KeyCode.D))
{
anim = StartCoroutine(SetWalkRight());
}

else if (Input.GetKey(KeyCode.S))
{
anim = StartCoroutine(SetWalkDown());
}
}

IEnumerator DoMovement(float x = 0, float y = 0)
{
Vector2 start = transform.position;
Vector2 end = new Vector3(start.x + x, start.y + y);

while (fraction < 1)
{
fraction += Time.fixedDeltaTime * speed;
transform.position = Vector2.Lerp(start, end, fraction);
yield return new WaitForSeconds(1f / speed);
}

transform.position = end;
anim = null;
fraction = 0;
}

IEnumerator SetWalkDown()
{
return DoMovement(0, -1);
}

IEnumerator SetWalkLeft()
{
return DoMovement(-1, 0);
}

IEnumerator SetWalkRight()
{
return DoMovement(1, 0);
}

IEnumerator SetWalkUp()
{
return DoMovement(0, 1);
}
}
``````

My first attempt at collisions was just to mimic the gameboy. Most gameboy games checked the tile in the direction the player was going to walk and used that to determine if the player could walk onto said tile. This would be most simple. I never could figure out how to get the tile the player would walk onto so I kind of created a hacked up system where it would ask the tilemap for the tile by offsetting the player and tile coordinates which was a horrible failure because the player had a pivot point inbetween two tiles and was in float rather the tilemap used ints and had a pivot point in the middle of each tile.

``````using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Tilemaps;

public class InputMovement : MonoBehaviour
{
public float speed = 60f;
private Coroutine anim = null;
private float fraction = 0;
public Tilemap collisionMap;
public Vector3Int gridOffset;

CollisionList list;

private void Start()
{
list = GameObject.FindObjectOfType<CollisionList>();
}

void FixedUpdate()
{
if (Input.GetKey(KeyCode.W) && !DoesCollide(0, 1, 0, .5f))
{
anim = StartCoroutine(SetWalkUp());
}

else if (Input.GetKey(KeyCode.A) && !DoesCollide(-1, 0, -.5f, 0))
{
anim = StartCoroutine(SetWalkLeft());
}

else if (Input.GetKey(KeyCode.D) && !DoesCollide(1, 0, .5f, 0))
{
anim = StartCoroutine(SetWalkRight());
}

else if (Input.GetKey(KeyCode.S) && !DoesCollide(0, -1, 0, -.5f))
{
anim = StartCoroutine(SetWalkDown());
}
}

bool DoesCollide(int x, int y, float fixX = 0, float fixY = 0)
{
int cellX = (int)(transform.position.x + fixX) + gridOffset.x + x;
int cellY = (int)(transform.position.y + fixY) + gridOffset.y + y;
Vector3Int vector = new Vector3Int(cellX, cellY, 0);
Sprite tile = collisionMap.GetSprite(vector);

bool match = false;
foreach (Sprite sprite in list.collision)
{
if (tile == sprite)
match = true;
}

return match;
}

IEnumerator DoMovement(float x = 0, float y = 0)
{
Vector2 start = transform.position;
Vector2 end = new Vector3(start.x + x, start.y + y);

while (fraction < 1)
{
fraction += Time.fixedDeltaTime * speed;
transform.position = Vector2.Lerp(start, end, fraction);
yield return new WaitForSeconds(1f / speed);
}

transform.position = end;
anim = null;
fraction = 0;
}

IEnumerator SetWalkDown()
{
return DoMovement(0, -1);
}

IEnumerator SetWalkLeft()
{
return DoMovement(-1, 0);
}

IEnumerator SetWalkRight()
{
return DoMovement(1, 0);
}

IEnumerator SetWalkUp()
{
return DoMovement(0, 1);
}
}
``````

It worked great 75% of the time else it half-worked or didn’t work at all so it was a total failure. My next attempt was with rigid body. I was forced to use dynamic even though kinematic would work better because, again, Unity feels too modern for this kind of system.

``````using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Tilemaps;

public class InputMovement : MonoBehaviour
{
public float speed = 60f;
private Coroutine anim = null;
private float fraction = 0;
private Rigidbody2D rb;

private void Start()
{
rb = GetComponent<Rigidbody2D>();
}

void FixedUpdate()
{
if (anim != null)
return;

if (Input.GetKey(KeyCode.W))
{
anim = StartCoroutine(SetWalkUp());
}

else if (Input.GetKey(KeyCode.A))
{
anim = StartCoroutine(SetWalkLeft());
}

else if (Input.GetKey(KeyCode.D))
{
anim = StartCoroutine(SetWalkRight());
}

else if (Input.GetKey(KeyCode.S))
{
anim = StartCoroutine(SetWalkDown());
}
}

IEnumerator DoMovement(float x = 0, float y = 0)
{
Vector2 start = transform.position;
Vector2 end = new Vector3(start.x + x, start.y + y);

while (fraction < 1)
{
fraction += Time.fixedDeltaTime * speed;
rb.MovePosition(Vector2.Lerp(start, end, fraction));
yield return new WaitForSeconds(1f / speed);
}

transform.position = end;
anim = null;
fraction = 0;
}

IEnumerator SetWalkDown()
{
return DoMovement(0, -1);
}

IEnumerator SetWalkLeft()
{
return DoMovement(-1, 0);
}

IEnumerator SetWalkRight()
{
return DoMovement(1, 0);
}

IEnumerator SetWalkUp()
{
return DoMovement(0, 1);
}
}
``````

This was a big failure because, although Collision worked, it would collide, jump and down on the screen and then collide into said object. Then it would jump all around and collide out. Total failure. I read it’s because I used MovePosition but if I switch to AddForce it’s not exactly going to work well either because while I’m sure it’ll work great it most certainly won’t feel like a retro tile-based game at all and instead a modern 2D game.

Like I said above, I just want to have the character move from tile to tile unless a collision were to happen and this seems nigh impossible with Unity.

Nevermind figured it out. I was close in my original attempt. Instead of offsetting the position to the grid and mucking around with that. I instead tracked which grid cells the player was over directly and when moving around I would update the grid cell equivelant. Since the player was over 2 grid cells I needed to keep track of both cells both feet were over and collision check both.

``````using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Tilemaps;

public class InputMovement : MonoBehaviour
{
public float speed = 60f;
private Coroutine anim = null;
private float fraction = 0;
public Tilemap collisionMap;
public Vector2Int leftTile;
public Vector2Int rightTile;

CollisionList list;

private void Start()
{
list = GameObject.FindObjectOfType<CollisionList>();
}

void FixedUpdate()
{
if (anim != null)
return;

if (Input.GetKey(KeyCode.W) && !DoesCollide(0, 1))
{
anim = StartCoroutine(SetWalkUp());
}

else if (Input.GetKey(KeyCode.A) && !DoesCollide(-1, 0))
{
anim = StartCoroutine(SetWalkLeft());
}

else if (Input.GetKey(KeyCode.D) && !DoesCollide(1, 0))
{
anim = StartCoroutine(SetWalkRight());
}

else if (Input.GetKey(KeyCode.S) && !DoesCollide(0, -1))
{
anim = StartCoroutine(SetWalkDown());
}
}

bool DoesCollide(int x, int y)
{
Vector3Int vectorL = new Vector3Int(leftTile.x + x, leftTile.y + y, 0);
Vector3Int vectorR = new Vector3Int(rightTile.x + x, rightTile.y + y, 0);

Sprite tileL = collisionMap.GetSprite(vectorL);
Sprite tileR = collisionMap.GetSprite(vectorR);

bool match = false;
foreach (Sprite sprite in list.collision)
{
if (tileL == sprite || tileR == sprite)
match = true;
}

return match;
}

IEnumerator DoMovement(int x = 0, int y = 0)
{
Vector2 start = transform.position;
Vector2 end = new Vector3(start.x + x, start.y + y);

while (fraction < 1)
{
fraction += Time.fixedDeltaTime * speed;
transform.position = Vector2.Lerp(start, end, fraction);
yield return new WaitForSeconds(1f / speed);
}

leftTile.x += x;
leftTile.y += y;

rightTile.x += x;
rightTile.y += y;

transform.position = end;
anim = null;
fraction = 0;
}

IEnumerator SetWalkDown()
{
return DoMovement(0, -1);
}

IEnumerator SetWalkLeft()
{
return DoMovement(-1, 0);
}

IEnumerator SetWalkRight()
{
return DoMovement(1, 0);
}

IEnumerator SetWalkUp()
{
return DoMovement(0, 1);
}
}
``````