本文章作为记录在一次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);
}
}
}
}
}