2
\$\begingroup\$

I am developing a game with Unity 2017. The game mechanics are as follow:

  • Classic top-down RPG;
  • Movements are: UP, DOWN, LEFT and RIGHT;
  • Player evolves in a 16*16 map;
  • He can dig in the direction he wants to make his way through the map one tile at a time;

At first I was using a very classical approach using tilemaps, RigidBody2D and BoxCollider2D on tiles and the player GameObjects.

Collision detections went fine and my tilemap looked pretty much like that (red being the walls and white the empty spaces):

Classic tilemap

The problem with this approach is that I have to keep in memory the state of 256 tiles in a List<Vector2f> and to create the 256 expected GameObjects representing either my walls (which a totally black), or my floor (the empty space).

Creating a black GameObject with its own BoxCollider2D for each of the tile seems a little bit overkill. And this brings me to the second iteration of my game: to keep in memory only the tiles the player digged.

Expected tilemap

What I would like to achieve efficiently, is to let the player evolve as long as he is colliding with an underlying tile, but stop moving as soon as he doesn't.

This is ok:

This is ok

This isn't, and player should be placed to previous state.

This isn't

I could take advantage of OnTriggerEnter2d and handle custom collision and response, but I have the feeling that something more simple and more optimized may exists somehwhere in the wild!

Thanks to any help provided!

\$\endgroup\$
  • \$\begingroup\$ Your approach works, but why don't you just store 1s and 0s in an array and do the collision test yourself. 2D squares are easy to collide and the array location can tell you the coordinates of the tile. If you need even more space, you could compress storage by grouping similar tiles e.g. if a 4x4 block of tiles is all 0s or 1s, it could be stored as if it was a single large tile. \$\endgroup\$ – Kyy13 Feb 6 '18 at 14:31
  • 1
    \$\begingroup\$ For more clarification currentTile.x = floor(char.x/tilesize); currentTile.y = floor(char.y/tilesize); then if player is trying to move right... If(tileArray[floor((char.x+stepsize)/tilesize), currentTile.y]==0) then char.x += stepsize. Then do that for all directions of movement. This way you can check just the tiles surrounding you instead of doing collision detection every frame with the entire map. \$\endgroup\$ – Kyy13 Feb 6 '18 at 14:36
  • \$\begingroup\$ I want to take advantage of collisions mechanism already available in Unity. Besides the walls and the empty spaces, the player will collide and interact with other layers. So I could indeed do that for each of the differents layers, but I am really looking for an elegant solution. \$\endgroup\$ – Aymeric Feb 6 '18 at 14:43
  • 1
    \$\begingroup\$ Okay, the elegant solution is slow, because you will be testing collision with every tile on the map instead of just the 4 walls around you. If your map isn't huge, it still won't matter though. Just set a variable like bool collision = false; then loop through your tiles using the collision detection... If a tile collides then set collision = true; then at the end of the loop if(!collision) step character back \$\endgroup\$ – Kyy13 Feb 6 '18 at 14:48
  • \$\begingroup\$ @Aymeric Listen to Kyy13: His/her idea is sound; yours is not elegant, it is clunky and potentially slow. It is overkill for what you are trying to do. GameObjects & Colliders create much more processing and memory overhead than you need here. Boolean neighbour checks in a 2D array are far cheaper. \$\endgroup\$ – Engineer Feb 10 '18 at 7:58
3
+50
\$\begingroup\$

About what you consider "elegant":

  • Every single GameObject & associated Transform component incurs overhead.
  • Every single Collider component incurs overhead. Concave mesh colliders are VERY costly.
  • Save your limited GameObject budget for those things that really need it, like characters, missiles, triggers, etc. Exceeding a certain number of GameObjects, things get slow, real fast.
  • There is a ton of custom code in every Unity project under the sun, that does not need these.

...They are excellent for more complex cases where you need flexibility. In your case that does not apply AT ALL. In fact, yours is the simplest case of collision detection & resolution in existence:

struct Tile
{
    bool isOpen;
    //whatever else you need, like terrain type etc.
}

Tile[,] tiles = new Tile[WIDTH, HEIGHT];

Vector2 north = new Vector2(0, -1);
Vector2 south = new Vector2(0, +1);
Vector2 east  = new Vector2(+1, 0);
Vector2 west  = new Vector2(-1, 0);

void HandleMoveAttempt(Vector2 direction)
{
    Vector2 proposedPos = playerPos + direction;
    Tile proposedTile = tiles[proposedPos.x, proposedPos.y];
    if (proposedTile.isOpen)
        playerPos += proposedPos;
    //else leave them where they are.
}

void Update()
{
    if (Input.GetKey(KeyCode.LeftArrow))
       direction = west;
    if (Input.GetKey(KeyCode.RightArrow))
       direction = west;
    if (Input.GetKey(KeyCode.UpArrow))
       direction = north;
    if (Input.GetKey(KeyCode.DownArrow))
       direction = south;

    HandleMoveAttempt(direction);
}

You'll notice that HandleMoveAttempt() either performs an instant move to the new cell, or does not even try to go there (else). Your setup may not require instantaneously moving from the centre of one cell to the centre of another (which is the simplest use case and hence why I supplied it here) but rather, the player may move in small increments via keys, mouse or gamepad, to gradually try to cross tile boundaries. That's OK: this style of collision detection and resolution still suits.

But in that case let me know so I can update the answer if still not clear.

\$\endgroup\$
  • \$\begingroup\$ Hey thanks for your answer! So to keep a limited number of GameObjects, you would apply space partitioning for the tiles that are currently displayed and reuse them from a sort of pool to prevent creating too many (ie: width * height at maximum) ? \$\endgroup\$ – Aymeric Feb 11 '18 at 20:58
  • \$\begingroup\$ P.S. Edited further, and will again if not clear. \$\endgroup\$ – Engineer Feb 12 '18 at 2:33
  • \$\begingroup\$ @Aymeric You're welcome. Assuming you're calling what I've done "space partitioning" (fair enough, I'd rather call it a collision grid or array): Not really, no. There are no GameObjects in my example, and there need not be. Basic arithmetic (array indexing) prevents the player from entering (implicitly-defined) spaces where they should not be able to go. Let me know if you have further questions. \$\endgroup\$ – Engineer Feb 12 '18 at 2:38
  • \$\begingroup\$ Hey Arcane! I'll accept your answer as it provides code, clear explanations and concepts. I implemented this logic and everything works fine! But I still have one question not directly related to this thread: is it possible to display a tile without using a Prefab in a Unity 2D game? I'll then apply space partitioning to only draw what is currently viewable on camera. \$\endgroup\$ – Aymeric Feb 12 '18 at 12:26
  • \$\begingroup\$ @Aymeric Thanks! Certainly there are ways to render graphics without using an individual GameObject per element, and without using prefabs. I would suggest setting up one GameObject / Mesh for your terrain, and then constructing vertices (with UVs for mapping textures) and triangles that represent your terrain as individual tiles. But that's a whole other topic. Good luck! \$\endgroup\$ – Engineer Feb 12 '18 at 12:29
0
\$\begingroup\$

Per the comments...

movePlayer();

bool collision = false;

for (i in numTiles)
{
    If (player collides with tile i) collision = true;
}

if (!collision) movePlayerBack();

I still recommend calculating the index of the tiles around you, and doing your collision detection with just the tiles around you.

\$\endgroup\$
  • \$\begingroup\$ Yes, that is called space partitioning. But I want to avoid using custom logic and loops as I suppose that BoxCollider2D and RigidBody2D already implement high end optimizations. \$\endgroup\$ – Aymeric Feb 6 '18 at 16:58
  • \$\begingroup\$ @Aymeric You'd be wrong in thinking that checking against a single convex mesh is cheaper than checking against several concave ones as Kyy13 suggests here, especially axis-aligned boxes which are about the cheapest possible thing to check against. I know as I have written convex-polygon collision detection code and it is very complex, with many nested corner cases to check (high branching cost). Frankly though, you should not be using colliders if you do it Kyy13's way. You should be doing 2D/3D array neighbour checks. \$\endgroup\$ – Engineer Feb 10 '18 at 7:46
0
\$\begingroup\$

This is going to be a code-less answer, but there should be enough here for you to write the code yourself.

So, rather than trying to use colliders to keep the player inside the collider volume, use colliders to do what colliders do best: keep the player out.

Instead of having every block have a collider (as this would be performance intensive for having hundreds of colliders that could never be collided with), only add colliders to the block adjacenet to empty spaces. When the player mines out a tile, destroy it, loop over the 8 adjacent tiles, and for each that a) still exists as a wall and b) does not already have a collider: add a box collider component. You may even only need to check the 4 orthogonally adjacent tiles, if your player can't move diagonally through a gap.

This will keep the performance low as well as keeping the complexity of your own code low.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.