Unity Tutorial - Tanks! 总结(下)
紧接着上一篇Unity Tutorial - Tanks! 总结(上),我们来到第五章
05 - SHELLS
现在有坦克有血条,自然而然就是要造炮弹了,对于炮弹首先要思考个问题,我们希望炮弹做什么,我们希望炮弹在碰撞其他的东西时,显示一个爆炸特效,除此之外,我们并不想要它会像一个实体一样爆炸后依然留在地上,为此我们需要为炮弹的 collider 中 Is Trigger
打上勾。这里解释一下 Is Trigger
,勾选它之后,炮弹将不会再被当作一个实体,它是一个只有表面,没有内容的模型,但是再加上 rigidbody 之后它的运动会符合物理规律,只是不会和任何其他的 collider 真实地发生碰撞,而是触发 OnTriggerEnter()
,OnTriggerExit()
,OnTriggerStay()
等方法。于是乎,这里的炮弹发生碰撞的行为我们可以理解为,在炮弹触发到 OnTriggerEnter()
方法时,显示爆炸效果,同时销毁炮弹实例。
知道炮弹是如何爆炸之后,接着来想象炮弹爆炸会引发哪些事情,可以想象得到,爆炸是一种范围性的影响,我们希望炮弹在爆炸的时候会对一定范围里面的敌人造成伤害,以及给它一个爆炸所产生的作用力,而对于其他的物体,我们不想对它们产生影响。这里使用的方法,是将坦克的 LayerMask 标记为 player 从而在爆炸范围监测时忽略掉其他的 collider ,结合源码感受一下具体实现
public class ShellExplosion : MonoBehaviour
{
public LayerMask m_TankMask; // Used to filter what the explosion affects, this should be set to "Players".
public ParticleSystem m_ExplosionParticles; // Reference to the particles that will play on explosion.
public AudioSource m_ExplosionAudio; // Reference to the audio that will play on explosion.
public float m_MaxDamage = 100f; // The amount of damage done if the explosion is centred on a tank.
public float m_ExplosionForce = 1000f; // The amount of force added to a tank at the centre of the explosion.
public float m_MaxLifeTime = 2f; // The time in seconds before the shell is removed.
public float m_ExplosionRadius = 5f; // The maximum distance away from the explosion tanks can be and are still affected.
private void Start ()
{
// If it isn't destroyed by then, destroy the shell after it's lifetime.
Destroy (gameObject, m_MaxLifeTime);
}
private void OnTriggerEnter (Collider other)
{
// Collect all the colliders in a sphere from the shell's current position to a radius of the explosion radius.
Collider[] colliders = Physics.OverlapSphere (transform.position, m_ExplosionRadius, m_TankMask);
// Go through all the colliders...
for (int i = 0; i < colliders.Length; i++)
{
// ... and find their rigidbody.
Rigidbody targetRigidbody = colliders[i].GetComponent<Rigidbody> ();
// If they don't have a rigidbody, go on to the next collider.
if (!targetRigidbody)
continue;
// Add an explosion force.
targetRigidbody.AddExplosionForce (m_ExplosionForce, transform.position, m_ExplosionRadius);
// Find the TankHealth script associated with the rigidbody.
TankHealth targetHealth = targetRigidbody.GetComponent<TankHealth> ();
// If there is no TankHealth script attached to the gameobject, go on to the next collider.
if (!targetHealth)
continue;
// Calculate the amount of damage the target should take based on it's distance from the shell.
float damage = CalculateDamage (targetRigidbody.position);
// Deal this damage to the tank.
targetHealth.TakeDamage (damage);
}
// Unparent the particles from the shell.
m_ExplosionParticles.transform.parent = null;
// Play the particle system.
m_ExplosionParticles.Play();
// Play the explosion sound effect.
m_ExplosionAudio.Play();
// Once the particles have finished, destroy the gameobject they are on.
ParticleSystem.MainModule mainModule = m_ExplosionParticles.main;
Destroy (m_ExplosionParticles.gameObject, mainModule.duration);
// Destroy the shell.
Destroy (gameObject);
}
private float CalculateDamage (Vector3 targetPosition)
{
// Create a vector from the shell to the target.
Vector3 explosionToTarget = targetPosition - transform.position;
// Calculate the distance from the shell to the target.
float explosionDistance = explosionToTarget.magnitude;
// Calculate the proportion of the maximum distance (the explosionRadius) the target is away.
float relativeDistance = (m_ExplosionRadius - explosionDistance) / m_ExplosionRadius;
// Calculate damage as this proportion of the maximum possible damage.
float damage = relativeDistance * m_MaxDamage;
// Make sure that the minimum damage is always 0.
damage = Mathf.Max (0f, damage);
return damage;
}
}
整个脚本的主要逻辑是遍历 Physice.OverlapSphere()
返回的一组 collider ,通过 GetComponent<Rigidbody>()
获取 targetRigidbody
来施加爆炸作用力,通过 GetComponent<TankHealth>
获取 targetHealth
来造成伤害。随后在销毁炮弹实例前,将原本作为炮弹子对象的爆炸特效抽离出来,播放完成后再进行销毁。
这里有一个问题我个人思考了很久,为什么将爆炸特效抽离出来不是用
m_ExplosionParticles.gameObject.parent = null
而使用m_ExplosionParticles.transform.parent = null
经过一番搜索后依然得不到答案,索性就自己改一下,发现m_ExplosionParticles.gameObject.parent = null
是不合法的,无法通过编译的,最后我强行猜想原因: 原本在 hierarchy 中,通过拖拽让一个对象变成另一个对象的子对象,他们之间的联系也就仅仅是通过在 transform 层面上建立起来的,所以通过将 transform.parent 置 null 相当于将他们建立联系的桥梁砍断,也意味着此时炮弹的销毁不再会影响到爆炸特效第二个需要注意的点是,脚本中的
CalculateDamage(Vector3 targetPosition)
方法之所以传入参数是一个 Vector3 是因为需要根据坦克和爆炸中心的距离比例来计算伤害。另外之所以要用到damage = Mathf.Max (0f, damage);
是因为 damage 有可能出现负数,出现负数的原因是坦克中心到爆炸范围的中心可能会大于爆炸半径,而同时又刚好能触发OnTriggerE