How to use Tilemap to create enemies (or terrain) and implement single tile damage determination

This article is used to record the problems encountered in a Game Jam and related experience methods.

Enemy requirements in the game:

The enemy is composed of blocks. If all the blocks are destroyed, the enemy will be defeated.

Planning needs:

The blocks that make up the enemy are implemented by Tilemap instead of grid adsorption. (The requirements are similar to implementing Tilemap to create destructible terrain)

problem analysis:

Only one Tilemap collider2D collision body is mounted on an enemy. How to determine the damage of all individual blocks.

Why use Tilemap to make enemies?

Because the enemies are made up of small blocks, and some enemies are made up of hundreds of blocks, it is inefficient to manually splice them one by one. So if you use Tilemap, you can quickly create a complete enemy.

What is so special about this requirement?

Under normal circumstances, damage determination is to mount a separate collision body for each object, and determine damage through collision detection. But Tilmapall tiles share a collider. In this case, if damage is directly applied to the enemy through collision detection, then all tiles They will all suffer the same damage, which obviously does not meet the requirements (the requirement is that a single tile performs independent damage determination).

Solutions

The condition for defeating the enemy is that the enemy is defeated when all the blocks it consists of are destroyed. So you should first get the total number of blocks (totalTiles) that make up the current enemy. Note that the blocks we want to obtain must be blocks with colliders, so that all non-enemies can be filtered out Components of tiles.

private void Start()
    {
        //初始化
        destroyBody = GetComponent<Tilemap>();
        tileHealth = new Dictionary<Vector3Int, int>();
        totalTiles = 0;

        foreach (var pos in destroyBody.cellBounds.allPositionsWithin)
        {
            if (destroyBody.HasTile(pos) && destroyBody.GetColliderType(pos) != Tile.ColliderType.None)
            {
                tileHealth.Add(pos, singleBodyHp);
                totalTiles++;
            }
        }
    }

There is a small pit at this location. There may be some transparent textures in the resources transferred from the art. Such places are parts that cannot be discovered by players in the game. The collision body needs to be changed to None to prevent the level from getting stuck.

        What can be imagined is that although there is only one collider, the detected object is always a complete enemy, but the contact point generated by collision detection is unique to a single tile. , so you can obtain the contact point through collision detection. Because the tilemap itself has a coordinate system different from the world coordinates, you need to use WorldToCell to convert the contact point coordinates into grid coordinates for later use. Eliminating individual tiles provides accurate location information.

        In the Start function, we use desroyBody.cellBounds.allPositionsWithin to store the current position information of all tiles together with the health value into the dictionary. When calling the function that applies damage, pass in the position information and damage value. If the health value of the current tile is 0, then eliminate a single tile through the SetTile method, then remove it from the dictionary, and reduce the total number of remaining tiles by one.

        The summary is that we do not directly determine the damage through collision detection, but indirectly obtain the collision point through collision detection, and then /span>, and then operate the single tile. Lock a single tile through the collision point

        This problem is not difficult, but I found it interesting when I encountered it for the first time, so I made this brief experience record.

The specific source code is as follows:

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

public class DestroyBody : MonoBehaviour
{
    //绘制敌人的Tilemap
    private Tilemap destroyBody;
    //用字典存储单个tile的坐标和血量
    private Dictionary<Vector3Int, int> tileHealth;
    //单个tile血量
    private int singleBodyHp;
    //击碎瓦片后的粒子特效
    public GameObject boomEffect;
    //组成敌人的总的瓦片数量
    private int totalTiles;
    //为了扩大伤害判定范围,添加X,Y方向偏移量
    public float offsetX;
    public float offsetY;

    private void Awake()
    {
        singleBodyHp = gameObject.transform.parent.GetComponent<EnemyMonster>().singleHp;
    }

    private void Start()
    {
        //初始化
        destroyBody = GetComponent<Tilemap>();
        tileHealth = new Dictionary<Vector3Int, int>();
        totalTiles = 0;

        foreach (var pos in destroyBody.cellBounds.allPositionsWithin)
        {
            if (destroyBody.HasTile(pos) && destroyBody.GetColliderType(pos) != Tile.ColliderType.None)
            {
                tileHealth.Add(pos, singleBodyHp);
                totalTiles++;
            }
        }
    }

    private void Update()
    {
        //如果瓦片总数 为0,判定此敌人已消灭
        if (totalTiles == 0)
        {
            Destroy(gameObject.transform.parent.gameObject);
        }
    }

    private void OnCollisionEnter2D(Collision2D collision)
    {
        if (collision.gameObject.CompareTag("PlayerBullet"))
        {
            //获取伤害值
            int damage = collision.gameObject.GetComponent<Attack>().damage;
            //获取碰撞点
            Vector3 hitPos = collision.contacts[0].point;

            //另外生成8个点,扩大伤害判定范围
            Vector3Int[] tilePositions = new Vector3Int[]
            {
                destroyBody.WorldToCell(hitPos),
                destroyBody.WorldToCell(hitPos + new Vector3(offsetX, 0f, 0f)),
                destroyBody.WorldToCell(hitPos - new Vector3(offsetX, 0f, 0f)),
                destroyBody.WorldToCell(hitPos + new Vector3(0f, offsetY, 0f)),
                destroyBody.WorldToCell(hitPos - new Vector3(0f, offsetY, 0f)),
                destroyBody.WorldToCell(hitPos + new Vector3(offsetX, offsetY, 0f)),
                destroyBody.WorldToCell(hitPos + new Vector3(offsetX, -offsetY, 0f)),
                destroyBody.WorldToCell(hitPos - new Vector3(offsetX, -offsetY, 0f)),
                destroyBody.WorldToCell(hitPos - new Vector3(offsetX, offsetY, 0f))
            };

            //调用受击函数
            foreach (Vector3Int tilePos in tilePositions)
            {
                TakeDamage(damage, tilePos, hitPos);
            }
        }
    }

    /// <summary>
    /// 接收伤害 
    /// </summary>
    /// <param name="damage"></param>
    /// <param name="tilePos"></param>
    /// <param name="boomEffectPos"></param>
    public void TakeDamage(int damage, Vector3Int tilePos, Vector3 boomEffectPos)
    {
        //当前tilemap中的tilePos处是否存在瓦片
        if (destroyBody.HasTile(tilePos))
        {
            if (tileHealth.ContainsKey(tilePos))
            {
                tileHealth[tilePos] -= damage;
                //当前位置瓦片血量小于0,且存在瓦片
                if (tileHealth[tilePos] <= 0 && destroyBody.GetTile(tilePos) != null)
                {
                    //移除此位置的瓦片
                    destroyBody.SetTile(tilePos, null);
                    //瓦片总数减一
                    totalTiles--;
                    AudioManager.Instance.PlaySound(AudioName.Sound_EnemyDead);
                    //播放击碎特效
                    GameObject effect = Instantiate(boomEffect, boomEffectPos, Quaternion.identity);
                    Destroy(effect, 1.5f);
                    //从字典中移除
                    tileHealth.Remove(tilePos);
                }
            }
        }
    }
}

Guess you like

Origin blog.csdn.net/m0_63673681/article/details/134096305