欢迎光临散文网 会员登陆 & 注册

用Unity实现传送门效果(二)

2018-11-12 20:57 作者:皮皮关做游戏  | 我要投稿

作者:四五二十


大家好。

上一期我们主要讲了多层空间画面是怎么渲染的。在理解了上篇的前提下,我们来继续做余下的功能。

这个项目一共只有5个脚本,抛开角色控制和人物动画管理两个脚本,主要讲解剩下三个:

我们先将两个传送门放到场景中主摄像机照不到的地方:

接着上一篇,为了达到三层空间渲染效果,一共创建四个辅助摄像机和四个Substitute,把它们平均分配给两个传送门作为子物体:

创建一个空物体DoorManager,再创建一个脚本将它们都管理起来:

public class DoorManager : MonoBehaviour

{

    public Transform mainCamera; //主摄像机

    public Transform[] substitutes; //替身

    public Transform[] Cameras; //辅助摄像机

    public Door[] doors; //传送门

 

编辑器里将它们拖进去:

然后在一个方法里同步它们的位置和旋转:

void SetSubstitutePos() //多层空间摄像机渲染

{

    //一层空间替身获取主摄像机坐标旋转

    substitutes[0].position = substitutes[1].position = mainCamera.position;

    substitutes[0].rotation = substitutes[1].rotation = mainCamera.rotation;

    //二层空间摄像机获取一层空间替身的本地坐标旋转

    Cameras[1].localPosition = substitutes[0].localPosition;

    Cameras[1].localRotation = substitutes[0].localRotation;

    Cameras[0].localPosition = substitutes[1].localPosition;

    Cameras[0].localRotation = substitutes[1].localRotation;

    //二层空间替身获取二层空间摄像机的坐标旋转

    substitutes[2].position = Cameras[1].position;

    substitutes[2].rotation = Cameras[1].rotation;

    substitutes[3].position = Cameras[0].position;

    substitutes[3].rotation = Cameras[0].rotation;

    //三层空间摄像机获取二层空间替身的本地坐标旋转

    Cameras[2].localPosition = substitutes[3].localPosition;

    Cameras[2].localRotation = substitutes[3].localRotation;

    Cameras[3].localPosition = substitutes[2].localPosition;

    Cameras[3].localRotation = substitutes[2].localRotation;

}

 

该方法放到LateUpdate里调用。

开启传送门

我们的墙是一面面拼成的,至于碰撞盒子为什么需要往外延伸,后面会讲到它的作用。

开启传送门就是朝墙上发射子弹,如果碰到墙就让该面墙的渲染禁用(看不见),打开碰撞器的触发功能(主角穿梭时不会被阻挡),让传送门出现在该墙的位置,且两扇传送门的本地坐标一个朝外,一个朝里。

我们用一个计数器来记录开门次数,根据计数单双来区别朝外和朝里,创建一个门的脚本Door,在里面写上开门的方法:

public class Door : MonoBehaviour

{

    public float angle; //旋转角度

    public void OpenDoor(Vector3 pos, Quaternion rota) //获取位置和旋转打开传送门

    {

        //获取新的位置和旋转

        transform.position = pos;

        transform.rotation = rota;

        transform.Rotate(0, angle, 0);

    }

 

两扇传送门分别都挂上,并且其中一个的angle变量在编辑器里设为180,区分朝里和朝外,然后在DoorManager脚本里调用:

    Transform[] walls = new Transform[2]; //保存开门时被隐藏的墙

    int number = 0; //计数器

    public void AddWall(Transform wall) //获取当前墙

    {

        int i = number % 2;

        if (walls[i] != null) //不为空则将之前隐藏的先显示

            ShowWall(walls[i], true);

        walls[i] = wall;

        if (number > 0)

        {

            ShowWall(walls[i], false); //隐藏当前

            if (number == 1)

                ShowWall(walls[0], false);

        }

        OpenDoor(i); //打开传送门

        number++;

    }

    void ShowWall(Transform wall, bool b) //隐藏墙

    {

        wall.GetComponent<BoxCollider>().isTrigger = !b; //开关触发器

        wall.GetComponent<SpriteRenderer>().enabled = b; //开关渲染器

    }

    void OpenDoor(int i) //打开传送门

    {

        doors[i].OpenDoor(walls[i].position, walls[i].rotation);

        pm.startColor = i == 0 ? Color.red : Color.blue;

    }

 

AddWall方法会在子弹碰到墙时调用。

接下来我们做子弹的功能,开启传送门的子弹我们在场景中只有一个,当它处于禁用状态时才能开枪发射,然后子弹墙或飞出地图一定距离时再禁用。

子弹我们用了一个移动时能产生拖尾的粒子效果,保留了碰撞器和刚体,为它挂上一个脚本:

public class Bullet : MonoBehaviour //子弹

{

    public float speed;

    Rigidbody rig;

    DoorManager dm; //传送门管理器

    Transform wall;

    bool open = true; //启动传送门也需要冷却时间

    void Start()

    {

        rig = GetComponent<Rigidbody>();

        dm = FindObjectOfType<DoorManager>();

    }

    void Update()

    {

        rig.velocity = transform.forward * Time.deltaTime * speed; //前进

       //飞出地图一定距离自动禁用(地图放在世界中心)

        if (Mathf.Abs(transform.position.z) > 8 || Mathf.Abs(transform.position.x) > 5)

            gameObject.SetActive(false); 

    }

    void OnCollisionEnter(Collision other) //碰撞一次

    {

        if (other.collider.CompareTag("Wall"))

        {

//撞到的墙不是刚才的墙,防止两道门开在同一面墙上

            if (open && wall != other.transform)          

 {

                open = false;

                wall = other.transform;

                dm.AddWall(other.transform);

                Invoke("ColdOpen", 0.2f); //0.2秒后完成冷却

            }

        }

        gameObject.SetActive(false);  

    }

    void ColdOpen()

    {

        open = true;

    }

 

传送主角

我们结合旁观者角度看看主角是怎么被传送的:

简单说,就是判断主角与某个传送门之间的位置,达到一定位置条件就将另一个传送门的位置赋给他,让主角出现在另一个传送门的位置。

我们知道主角的位置和旋转给了主摄像机,而主摄像机的位置和旋转又给了第一层空间的两个substitute,而两个substitute又分别是两扇传送门的子物体,所以只需要判断substitute的本地坐标就行了,将传送主角的方法写在DoorManager脚本里:

    void DeliveryPlayer() //传送主角

    {

        if (number >= 2) //有两道门后可以执行传送

        {

            DeliveryCondition(0, substitutes[0].localPosition.z > 0);

            DeliveryCondition(1, substitutes[1].localPosition.z < 0);

        }

    }

    void DeliveryCondition(int i, bool b) //传送主角条件

    {

        int j = Mathf.Abs(i - 1); //另一道门的索引

        //判断某个一层替身与父物体(传送门)的位置关系

        if (Mathf.Abs(substitutes[i].localPosition.x) < 0.3f && Mathf.Abs(substitutes[i].localPosition.y) < 1 && b)

        {

            //将主角传送至另一道门位置

            player.position = Cameras[j].position;

            Quaternion r = Cameras[j].rotation;

            player.rotation = new Quaternion(player.rotation.x, r.y, player.rotation.z, r.w);

        }

    }

 

把DeliveryPlayer方法也放在LateUpdate里实时监测。

传送子弹

传送子弹使用触发的方式,当传送门被打开时,原本位置的墙会隐藏并开启触发器,我们之前使用了一个数组专门用来保存隐藏的墙,我们只需要在触发时识别其中一个,然后立刻传送到另一个的位置就行了,传送子弹的方法我们写在DoorManager里:

    public void DeliveryBullet(Transform bullet, Transform wall) //传送子弹

    {

        bullet.parent = wall; //获取该墙成为子弹父物体

        //保存自身本地坐标和旋转

        Vector3 lp = bullet.localPosition;

        Quaternion lr = bullet.localRotation;

        //让另一道门成为子弹父物体

        if (wall == walls[0])

            bullet.parent = walls[1];

        else

            bullet.parent = walls[0];

        //将刚才的本地坐标和旋转再赋予子弹

        bullet.localPosition = new Vector3(-lp.x, lp.y, -lp.z);

        bullet.localRotation = lr;

        bullet.Rotate(0, 180, 0, Space.World);

    }

 

然后在子弹的脚本里使用触发方式调用:


    void OnTriggerEnter(Collider other) //触发一次

    {

        wall = other.transform; //获取被触发的墙

        if (wall != transform.parent) //触发的物体(墙)不是自己的父物体

            dm.DeliveryBullet(transform, wall); //传送子弹

    }

 

传送门动画

暂停画面会看到在同一个画面中出现了两个门,说明每个门都还有一个替身,当门的位置发生改变时,原来的门会变小,然后以变大的方式呈现,原位置会用假门来替代,假门出现后会缩小并消失,在场景中导入两个假门放在玩家看不见的地方,然后在Door的脚本里添加动画功能:

    Vector3 pos; //位置

    Vector3 scale;//大小

    public float angle; //旋转角度

    public Transform CopyDoor; //把假门拖进去

    void Start()

    {

        //记录初始位置和大小

        pos = transform.position;

        scale = transform.localScale;

    }

    void Update()

    {

        //位置发生改变

        if (pos != transform.position)

        {             

            //更新位置和旋转信息

            pos = transform.position;

            transform.localScale = Vector3.zero;

        }

        //真门变大动画

        transform.localScale = Vector3.Lerp(transform.localScale, scale, Time.deltaTime * 10);

 

        if (CopyDoor.gameObject.activeInHierarchy) //如果假门被启用调用展示动画

            ShowPrefabDoor();

    }

    void DisplayDoor() //显示假门

    {

        //首先复制真门的位置旋转尺寸,然后启用

        CopyDoor.position = pos;

        CopyDoor.rotation = transform.rotation;

        CopyDoor.localScale = scale;

        CopyDoor.gameObject.SetActive(true);

    }

    void ShowPrefabDoor() //展示假门动画

    {

        //假门变小

        CopyDoor.localScale = Vector3.Lerp(CopyDoor.localScale, Vector3.zero, Time.deltaTime * 10);

        if (CopyDoor.localScale.x < 0.1f) //小到一定程度就禁用

            CopyDoor.gameObject.SetActive(false);

    }

 


我们来看一下慢动作效果:

接下来就说说会遇到的坑,首先就是当我们走到这个位置来的时候:

或者

这些都是第二层空间两个摄像机的渲染层级没设置好造成的。

我们在DoorManager写了一个方法,会根据两个摄像机的位置实时得去调整他们的渲染层级:


void SwitchCameraDepth() //切换二层空间摄像机层级

    {

        //如果两个一层替身本地高度差不多,哪个替身离父物体Z轴距离近,一起的摄像机层级越低

        if (Mathf.Abs(substitutes[0].localPosition.y - substitutes[1].localPosition.y) < 0.1f)

        {

            if (Mathf.Abs(substitutes[0].localPosition.z) < Mathf.Abs(substitutes[1].localPosition.z))

                SetDepth(Cameras[0], Cameras[1]);

            else

                SetDepth(Cameras[1], Cameras[0]);

        }

        else //如果高度相差很大,哪个替身与父物体Y轴距离小,一起的子物体摄像机层级越低

        {

            if (Mathf.Abs(substitutes[0].localPosition.y) < Mathf.Abs(substitutes[1].localPosition.y))

                SetDepth(Cameras[0], Cameras[1]);

            else

                SetDepth(Cameras[1], Cameras[0]);

        }

    }

    void SetDepth(Transform camera1, Transform camera2) //设置二层空间两个摄像机渲染层级

    {

        camera1.GetComponent<Camera>().depth = -3;

        camera2.GetComponent<Camera>().depth = -2;

    }

 

然后放在LateUpdate里调用就可以解决刚才的问题了。

第二个坑是这里,刚才我们提到为什么墙的碰撞盒子是这样:

主要是为了防止这种情况的发生:

当我们走到门口发射时,原计划要打到“前面”的墙,却因为子弹产生的位置碰不到触发器,导致子弹无法被成功传送,这样就不能再“前方”墙上产生新的传送门。

解决办法自然就是加厚碰撞器,让主角走到很边缘的位置也能保证子弹能被传送。

坑点主要就是这两个。

以此为基础可以延伸出许多玩法,大家可以开开脑洞,或者研究一下更复杂地形中传送门功能的实现。

下面是原工程地址,里面有工程素材,以及人物控制和动画的脚本:

https://github.com/wushupei/TransmissionGate


最后想系统学习游戏开发的童鞋,欢迎访问 http://levelpp.com/       

游戏开发搅基QQ群:869551769     

微信公众号:皮皮关


我们这篇文章暂时就到这里,谢谢各位。

用Unity实现传送门效果(二)的评论 (共 条)

分享到微博请遵守国家法律