使用Tilemap创建敌人(或地形)并实现单个瓦片伤害判定的方法

本文章作为记录在一次Game Jam中遇到的问题以及相关经验方法。

游戏中敌人需求:

敌人由方块组成,消灭所有组成方块,敌人被击败。

策划需求:

组成敌人的方块由Tilemap实现,而非网格吸附。(需求类似于实现Tilemap创建可破坏地形)

问题分析:

一个敌人身上只挂载了一个Tilemap collider2D碰撞体,如何实现所有单个方块的伤害判定。

为什么要用Tilemap制作敌人?

因为敌人由一个个小的方块组成,有的敌人由上百个方块组成,人力手动去一个一个对着拼接,效率低下。所以如果用Tilemap,就可以快速创建一个完整的敌人。

这个需求特殊在哪?

一般情况下的伤害判定是每个物体单独挂载一个碰撞体,通过碰撞检测进行伤害判定。但是Tilmap的所有瓦片共用一个碰撞体,在这种情况下,如果直接通过碰撞检测对敌人施加伤害,那么所有的瓦片都会受到同样的伤害,这显然是不符合需求的(需求是单个瓦片进行单独的伤害判定)

解决思路

击败敌人的条件是,所有组成的方块全部消灭则敌人被击败。所以应该首先获取组成当前敌人的方块总数(totalTiles)。注意我们要获取的方块一定是带有碰撞体的方块,这样就能过滤掉所有非敌人组成部分的瓦片。

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++;
            }
        }
    }

这个位置有一个小坑,美术传过来的资源中,可能会存在有一些透明贴图的情况,这样的地方属于在游戏中玩家无法发现的部分,需要将其碰撞体改为None,防止卡关。

        可以想到的是,虽然只有一个碰撞体,检测到的物体永远是一个完整的敌人,但是碰撞检测产生的接触点对于单个瓦片是唯一的,所以就可以通过碰撞检测获取接触点,因为tilemap自身拥有一个不同于世界坐标的坐标系,需要通过WorldToCell,将接触点坐标转换为网格坐标,来为后面消除单个瓦片提供准确的位置信息。

        而在Start函数中我们就通过desroyBody.cellBounds.allPositionsWithin来将当前所有瓦片的位置信息连同生命值存储到字典中。在调用施加伤害的函数时,传入位置信息和伤害值,如果当前位置瓦片的生命值为0,则通过SetTile方法消除单个瓦片,然后将其从字典中移除,剩余瓦片总数减一即可。

        总结就是,我们不直接通过碰撞检测进行伤害判定,而是间接通过碰撞检测获取到碰撞点,然后再通过碰撞点锁定单个瓦片,对单个瓦片进行操作即可。

        本问题称不上难点,但是第一次遇到觉得挺有意思,便作此简短的经验记录。

具体源码如下:

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);
                }
            }
        }
    }
}

猜你喜欢

转载自blog.csdn.net/m0_63673681/article/details/134096305