1
\$\begingroup\$

I have a megaman-style platformer that I've been working on for a while now. I'd like to have my scene set up so that when a player interacts with certain tiles, it will have some sort of effect on the player.

This could easily be achieved with creating individual sprites for the aforementioned interactables, but I'd ideally like to be able to have a portion of my script handle it so that everything besides the player and enemies would be tilemaps.

For instance, I have a tile that would act as a treadmill that moves the player along in one direction. Another tile would be bouncy, and another would deal damage.

I have the game set up right now to detect when the player is touching a tile, but I have no way of being able to tell which tile the player is touching. Is there a way to be able to compare programmatically which tile from a tile palette my player's hitbox is currently touching?

\$\endgroup\$
1
\$\begingroup\$

If you want to know what tile is at a particular location, you can just ask the tilemap. Then you can use that tile to look up into a mapping of which effect to call:

Inspector example of the script below

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

public class TileCollisionEffect : MonoBehaviour
{
    // Define what kind of script methods can handle our tile collision events.
    [System.Serializable]
    public class CollisionEvent : UnityEvent<Collision2D> { }

    // Create a data structure to pair up a particular tile with a particular effect.
    [System.Serializable]
    public struct TileEffect {
        public TileBase tile;
        public CollisionEvent effect;
    }

    // Expose in the inspector a list of tile-effect mappings.
    public TileEffect[] effects;
    Dictionary<TileBase, CollisionEvent> _effectMap;

    // Pack our map of tile effects into a dictionary for ease of lookups.
    private void OnEnable() {
        if (_effectMap != null)
            return;

        _effectMap = new Dictionary<TileBase, CollisionEvent>(effects.Length);
        foreach (var entry in effects)
            _effectMap.Add(entry.tile, entry.effect);
    }

    private void OnCollisionEnter2D(Collision2D collision) {
        // If you know in advance what tilemap you're going to collide with,
        // you can cache this reference instead of searching for it with GetComponent.        
        var map = collision.collider.GetComponent<Tilemap>();
        var grid = map.layoutGrid;

        // Find the coordinates of the tile we hit.
        var contact = collision.GetContact(0);
        Vector3 contactPoint = contact.point - 0.05f * contact.normal;
        Vector3 gridPosition = grid.transform.InverseTransformPoint(contactPoint);
        Vector3Int cell = grid.LocalToCell(gridPosition);

        // Extract the tile asset at that location.
        var tile = map.GetTile(cell);

        if(tile == null)
            return; // No valid tile! Abort!

        // Check if we have an effect for this tile type. If so, fire it!
        if (_effectMap.TryGetValue(tile, out CollisionEvent effect) && effect != null)
            effect.Invoke(collision);
    }

    public void Conveyor(Collision2D collision) {
        // TODO: apply conveyor effect.
        Debug.Log("Conveyor effect!");
    }

    public void Bounce(Collision2D collision) {
        // TODO: apply bouncy effect.
        Debug.Log("Bounce effect!");
    }
}
\$\endgroup\$
8
  • \$\begingroup\$ I think I'm tracking. Just a few quick questions, however. In the inspector, would a separate tile effect script be added below the "runtime only" drop-down box? If so, when selecting the function in the drop-down box to the right, how would you reference the function TileCollisionEffect.Bounce, for example? Or would you add the object that you apply the script to? \$\endgroup\$ May 26 '20 at 19:57
  • \$\begingroup\$ Also, I'm adding this script to the grid that the tilemap is in, right? Sorry for all the questions, I'm just doing my best to understand, haha \$\endgroup\$ May 26 '20 at 20:01
  • \$\begingroup\$ In my test, this script was on the character object doing the colliding. You can put it anywhere you want, write the bounce method anywhere you want, chop this code into any number of pieces and put them wherever it makes sense for your project. Any solution that achieves your ends is valid. The only slightly tricky bits are fetching the tile from the contact point, and exposing a list of tile-to-handler-method mappings, which you can see demonstrated in one form here, or modify them to implement these parts differently to suit your project. \$\endgroup\$
    – DMGregory
    May 26 '20 at 20:24
  • \$\begingroup\$ I think the issue might be in the dictionary. When I run the game, I get an argumentNullException saying that the key cannot be null. It seems to be happening on the line: if (_effectMap.TryGetValue(tile, out CollisionEvent effect) && effect != null). \$\endgroup\$ May 26 '20 at 22:23
  • \$\begingroup\$ Looks like you have a null tile. This didn't show up in my tests, but it might happen if your tile colliders go outside their grid cells. \$\endgroup\$
    – DMGregory
    May 26 '20 at 22:25

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.