先放效果和实现目标:

  • 激光武器有攻击范围,超出范围的对象不会与激光发生碰撞;
    激光武器有一个射击范围,当且仅当范围内的敌人会受到伤害
  • 在攻击范围内的对象会与激光发生碰撞并且阻挡激光。同时,考虑到特殊情况,能与激光发生碰撞的对象应该是可编程的;
    激光无法穿过墙壁
  • 在操作上,激光向鼠标点击方向发射,鼠标松开后不再发射。同时,为了让激光发射的过程看起来更加的自然,应该在激光消失前添加一个逐渐缩小的效果;
    平滑消失

一、 创建一个激光对象

  1. 为了提高复用性,构造Prefab对象,在使用时直接生成到世界坐标系中。所以先构造一个激光的Prefab;

  2. 选取一个合适的Renderer。有两种Renderer比较适合做2D的激光效果,它们是LineRenderer和SpriteRenderer。LineRenderer的好处是不用自己画素材,激光拉伸时不会出现畸变,坏处是丑,要好看的话得自己写Shader;而SpriteRenderer的好处是好不好看,美工来干,坏处是激光拉伸会畸变。我们简单的了解一下这两种Renderer;

  • LineRenderer的构造如下图。先创建Line对象,再修改右侧Inspector面板红框里的参数,让Line垂直于X轴,之后在inspector面板里改颜色,改材质。可以看到,用现有的材质效果并不太好;
    构造Line
    让Line垂直x轴
    改Line的参数
  • SpriteRenderer的构造如下图。创建完对象之后,直接修改Sprite就好了(inspector面板中的其他组件之后会讲)。在之后的步骤中,我们都直接使用SpriteRenderer的激光对象;
    创建Sprite
    改变sprite

二、添加碰撞体与碰撞检测。

进行实际操作之前,我们还需要了解Unity当中的几个属性和方法:

  • Physics2D.Raycast()。这个方法通过射线投影的方式,直接获取射线方向上第一个含有碰撞体组件的对象的引用。这个方法本身不能通过tag这种灵活的方式来控制是否碰撞,所以难以实现穿透敌人却穿不透墙壁的功能;
  • Physics2D.RaycastAll() 。这个方法相当灵活,它可以获取射线方向,射线范围内的所有含有碰撞体组件的对象的引用。通过获取对象数组的方式,我们进行二次的排除,便可以判断什么情况下碰撞,什么情况下穿透。另外,这个方法可以添加一个distance参数,使得从起点到终点距离大于distance的物体不会被检测,这刚好符合我们激光武器具有攻击范围的要求;
  • RaycastHit2D中的distance属性。Physics2D.RaycastAll()返回的结果是基于RaycastHit2D的对象数组。如果我们需要获取第一个有效碰撞体,那么就需要对碰撞体的距离进行排序,取到与发射者距离最近的那个物体,distance属性表示的就是射线的起点到碰撞体的距离。
  1. 把Prefab的tag设置为Bullet(其他的tag什么也行,这是用来判断是否碰撞的);
    设置tag

  2. 在当前Prefab的右侧面板中,分别加入Rigidbody2D、BoxCollider2D,把BoxCollider2D的"Is trigger"勾选上并且修改的碰撞体大小(加这两个组件的主要原因是可以实现激光的对波效果,如果希望激光会互相穿透,不加也可以);

  3. 新建一个组件,命名为“Laser Weapon Listenser”。这个类主要用来让发射后的激光对象能够独立的控制自己的方向和形状,与发射者对象实现解耦。该类的总体思路是:先获取RaycastAll的所有碰撞对象,再通过距离排序法获取第一个碰撞物体,并将原长度的激光Prefab的y轴压缩至distance的长度,在玩家释放鼠标后,压缩激光Prefab的x轴,使之逐渐消失。碰撞类的内部的完整代码如下;

using System.Collections.Generic;
using UnityEngine;

/// <summary>
/// 激光监听器脚本。
/// </summary>
public class LaserWeaponListener : MonoBehaviour
{
    #region 成员变量
    [SerializeField] [Tooltip("激光发射者的标签。")] public string brickTag = null;
    [SerializeField] [Tooltip("激光攻击频率。")] public float attackRate = 0.1F;
    [SerializeField] [Tooltip("激光攻击范围。")] public float attackRange = 1F;
    [SerializeField] [Tooltip("激光prefab对象的实际长度。")] public float length = 1F;

    private Vector3 ray = Vector3.zero;
    private Vector3 scale = Vector3.zero;
    private bool isAttacking = false;
    private float fadeOutRatio = 0.02F; // 攻击结束后,激光宽度的缩减系数
    private float countTime = 0;        // 攻击频率计时器
    private LaserWeaponController laserWeaponController = null;
    #endregion

    #region 属性控制
    /// <summary>
    /// 射击方向。
    /// </summary>
    public Vector3 Ray
    {
        get
        {
            return ray;
        }
        set
        {
            ray = value;
        }
    }

    /// <summary>
    /// 是否处于攻击状态。
    /// </summary>
    public bool IsAttacking
    {
        get
        {
            return isAttacking;
        }
        set
        {
            isAttacking = value;
        }
    }

    /// <summary>
    /// 激光发射者控制器。
    /// </summary>
    public LaserWeaponController LaserWeaponController
    {
        get
        {
            return laserWeaponController;
        }
        set
        {
            laserWeaponController = value;
        }
    }
    #endregion

    #region 基础私有方法
    /// <summary>
    /// 在帧刷新时触发。
    /// </summary>
    private void Update()
    {
        transform.position = laserWeaponController.gameObject.transform.position;	// 把激光发射点定在发射者原点
        if (isAttacking)
            Attacking();
        else
            DeAttacking();
        isAttacking = false;
    }

    /// <summary>
    /// 当触发器检测到物理接触时触发。
    /// </summary>
    /// <param name="collision">碰撞物体对象。</param>
    private void OnTriggerStay2D(Collider2D collision)
    {
        if (!CanCollide(collision.gameObject))
            return;
        if (Time.time > countTime)
            countTime = Time.time + attackRate;
        // 以下为目标对象收到伤害的代码,略
    }

    /// <summary>
    /// 攻击。
    /// </summary>
    private void Attacking()
    {
        RaycastHit2D[] hits = Physics2D.RaycastAll(transform.position, ray, attackRange);
        List<RaycastHit2D> colliders = new List<RaycastHit2D>();
        for (int i = 0; i < hits.Length; i++)
            if (hits[i] && CanCollide(hits[i].transform.gameObject))
                colliders.Add(hits[i]);
        float distance = GetNearestHitDistance(colliders);
        scale = new Vector3(1, distance / length, 1);	// 压缩激光的y轴长度
        transform.localScale = scale;
    }

    /// <summary>
    /// 去攻击。
    /// </summary>
    private void DeAttacking()
    {
        scale = new Vector3(scale.x - fadeOutRatio, scale.y, 1);
        transform.localScale = scale;
        if (scale.x <= 0)
            gameObject.SetActive(false);
    }

    /// <summary>
    /// 得到所有与射线碰撞的对象中,碰撞模长最小的线段长度。
    /// </summary>
    /// <param name="hits">射线碰撞对象列表。</param>
    /// <returns>碰撞模长最小的线段长度。</returns>
    private float GetNearestHitDistance(List<RaycastHit2D> hits)
    {
        float minDist = attackRange;
        for (int i = 0; i < hits.Count; i++)
            if (hits[i].distance < minDist)
                minDist = hits[i].distance;
        return minDist;
    }

    /// <summary>
    /// 判断当前对象是否能够与碰撞体发生碰撞。
    /// </summary>
    /// <param name="collider">碰撞体对象。</param>
    /// <returns>true则发生碰撞,false则不发生。</returns>
    private bool CanCollide(GameObject collider)
    {
        // 当碰撞体对象是激光时
        if (collider.TryGetComponent(out LaserWeaponListener weaponListener))
        {
            // 同源武器不碰撞
            if (brickTag == weaponListener.brickTag)
                return false;
        }
        else
        {
            // 己方不碰撞
            if (collider.tag == brickTag)
                return false;
        }
        return true;
    }
    #endregion
}

  1. 监听器已经构造好了,但是它只考虑对象构造之后的事情,至于在什么时机下构造对象,则需要额外建立一个类来实现。我们随便构造一个物体当做玩家对象,并在上面新建一个组件,名为“LaserWeaponController”。这个类的主要工作就是:获取玩家的鼠标点击,计算发射者与鼠标点击的射线方向,并将发射者本体的tag和射线方向一并传给LaserWeaponListener,让它执行后续操作。具体代码如下:
using UnityEngine;

/// <summary>
/// 激光控制脚本类。
/// </summary>
public class LaserWeaponController : MonoBehaviour
{
    #region 成员变量
    [SerializeField] [Tooltip("Prefab模板对象。")] public GameObject prefab = null;

    private GameObject laserObject = null;                      // 实际激光对象
    private ObjectPool objectPool = ObjectPool.GetInstance();   // 此处使用了对象池来控制物体的生成
    #endregion

    #region 公有方法
    /// <summary>
    /// 向对应方向发射激光。
    /// </summary>
    /// <param name="ray">射击方向向量。</param>
    public void Attack(Vector3 ray)
    {
        // 激光武器只会实例化唯一的对象
        if (laserObject == null)
            laserObject = objectPool.GetObject(prefab.name, transform.position, transform.rotation);
        laserObject.SetActive(true);
        // 初始化射击方向
        if (ray.y < 0)
            laserObject.transform.localEulerAngles = new Vector3(0, 0, Vector2.Angle(new Vector2(-1, 0), ray) + 90);
        else
            laserObject.transform.localEulerAngles = new Vector3(0, 0, Vector2.Angle(new Vector2(1, 0), ray) - 90);
        // 初始化监听器属性
        laserObject.GetComponent<LaserWeaponListener>().brickTag = gameObject.tag;
        laserObject.GetComponent<LaserWeaponListener>().Ray = ray;
        laserObject.GetComponent<LaserWeaponListener>().IsAttacking = true;
        laserObject.GetComponent<LaserWeaponListener>().LaserWeaponController = this;
    }
    #endregion

    #region 私有方法
    /// <summary>
    /// 在第一帧前触发
    /// </summary>
    private void Start()
    {
        // 此处采用了先构造对象模板,再通过对象池控制物体的生成和销毁。
        // 这种方法好处是,如果场景中存在多个角色发射激光,就不会反复的调用Destroy()方法,减少了计算开支
        prefab = Resources.Load("LASER") as GameObject;
    }

    /// <summary>
    /// 在帧刷新时触发。
    /// </summary>
    private void Update()
    {
        if (Input.GetMouseButton(0))
        {
            Vector3 ray = Camera.main.ScreenToWorldPoint(Input.mousePosition) - transform.position;
            Attack(ray);
        }
    }

    /// <summary>
    /// 当脚本失效时触发。
    /// </summary>
    private void OnDisable()
    {
        try
        {
            objectPool.ReleaseObject(laserObject);
        }
        catch
        {
            Debug.Log("Object is already been destroyed!");
        }
    }
    #endregion

    #region 静态方法
    /// <summary>
    /// 获取归一化后的方向向量。
    /// </summary>
    /// <param name="ray">射线方向。</param>
    /// <returns>归一化后的二维向量。</returns>
    private static Vector2 NormalDirection(Vector2 ray)
    {
        float dist = Mathf.Sqrt(ray.x * ray.x + ray.y * ray.y);
        if (dist == 0)
            return Vector2.zero;
        return new Vector2(ray.x / dist, ray.y / dist);
    }
    #endregion
}

三、附加内容

  • 当完成上述操作之后,只需改变tag就可以得到如下图的效果:
    最终效果
  • 但是此时仍然有一些不足,例如通过压缩y轴尺寸改变激光长度,会使得焰头和焰尾畸变,其实有一个比较简单的解决方案,既使用动态Sprite裁切的方法,直接把超过的部分给删除,而不是改变尺寸;
  • 另外,被攻击物体没有反馈,看起来很单调,这个问题的解决方案就是,在碰撞体的坐标位置添加一个爆炸效果或者烧灼效果的对象,发生碰撞时显示这个对象,不发生碰撞后便隐藏这个对象。
Logo

技术共进,成长同行——讯飞AI开发者社区

更多推荐