Unity实现传送门效果

接下来的两篇内容,是用Unity来做传送门的效果。这里是用自己的方法来做的,中途踩了许多坑不过最终还是实现了。下面是效果图:


前半段内容几乎没有代码。主要是介绍一下实现的原理。

先不说如何打开传送门以及如何在传送门中穿梭的问题,我们先来看看摄像机是怎样将场景及主角从另一个角度渲染进门里面的。

在现实生活中,如果把眼睛比作主摄像机,人的眼睛是不能直接看见自己的侧面的,如果一定要看则会用另外的摄像头从侧面对自己进行拍摄,再吧显示器材放在视线范围内,这样就能实时的去观察自己的侧面了。同样的道理,要达到上图中的效果,一个摄像机是肯定不够的,需要其它的摄像机来辅助。

来看看我们的场景,场景很简单,就是一个墙面的地面组成的,当传送门打开时,就让该处的墙暂时禁用:

让主摄像机作为主角的眼睛始终跟随主角移动,如果主摄像机从传送门看出去,门里其实是什么都没有的:

那么这段“空白”就需要填补,刚才我们提到了其它的摄像机,就是用来填补这个空白的。

在效果展示中,主角从橙色门中由内而外看出去,是和辅助摄像机从蓝色门由外到内看进来的画面是一样的:

所以只要将辅助摄像机看到的画面拿来填补主摄像机的空缺就可以了,前提是主摄像机的渲染层级更高,我们对主摄像机的属性进行设置:

但是要真正的无缝衔接则对摄像机的位置关系有一定的要求,也就是说主摄像机与橙色门的位置关系同辅助摄像机与蓝色门的位置关系必须是一致的:

要让辅助摄像机的位置同步,我们可以创建一个空物体,取名叫Substitute,让它成为橙色门的子物体,再创建一个辅助摄像机,取名Camera_1,的视锥范围要和主摄像机一样,但渲染层级较低,将Camera_1作为蓝色门的子物体:

注意这里为了方便理解使用了中文,在实际开发中请避免使用中文命名。

通过代码让Substitute实时获取主摄像机的世界坐标的位置与旋转,再将本地坐标赋给Camera_1,这样辅助摄像机与蓝色门的位置和主角与橙色门的位置就会保持同步:

[AppleScript] 纯文本查看 复制代码

?


1

2

3

4

//获取主摄像机世界位置和旋转

substitute.position=mainCamera.position;

substitute.rotation =mainCamera.rotation;[/color][/size][/font][/align][align=left][font=微软雅黑][size=3][color=rgb(26,26,26)]//将本地位置和旋转赋给辅助摄像机

camera_1.localPosition =substitute.localPosition;

camera_1.localRotation =substitute.localRotation;

同样的方法,再创建一个替身和辅助摄像机,也可以用来渲染蓝色门内的场景:

第三层空间的渲染

如果把主场景成为第一层空间,传送门看进去的是第二层,那么上图中红框内渲染的就是第三层。做到第三层空间的渲染,其实还是和渲染第二层一样的原理,创建渲染第三层空间的辅助摄像机Camera_2:

Camera_1与橙色门的位置关系,就是Camera_2与蓝色门的位置关系。

同样颜色的线代表相同的位置关系

同样的方法,将第三层的另一个摄像机也创建好,我们来看三层空间的效果展示:


这样就保持着三层空间的渲染,当然也可以再继续往下发展,除第一层空间外,每层都需要两个摄像机,方法都是一样,越往下层的摄像机渲染层级越低,例如主摄像机的层级为0,那么第二层空间的两个摄像机分别为-1和-2,第三层为-3和-4,且最低的摄像机的ClearFlags选项要设为Skybox或者SolidColor,其它所有摄像机都设置为Depth only。

而每个辅助摄像机都需要一个Substitute来帮助定位,如果要渲染三层空间,就一共需要四个Substitute(四个辅助摄像机)。

本期文章内容不多,主要是介绍了传送门的制作原理。当大家理解之后,在下一期中将会正式介绍物体在门中穿梭的实现方法,届时会少不了代码,并且同时会介绍过程中会遇到的许多坑。


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

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

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

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

[AppleScript] 纯文本查看 复制代码

?


1

2

3

4

5

public classDoorManager :MonoBehaviour

{

    public Transform mainCamera; //主摄像机

    public Transform[] substitutes; //替身

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

    public Door[] doors; //传送门

编辑器里将它们拖进去:

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

[AppleScript] 纯文本查看 复制代码

?


01

02

03

04

05

06

07

08

09

10

11

12

13

14

15

16

17

18

19

20

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,在里面写上开门的方法:

[AppleScript] 纯文本查看 复制代码

?


1

2

3

4

5

6

7

8

9

public classDoor :MonoBehaviour

{

    public float angle; //旋转角度

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

    {

        //获取新的位置和旋转

        transform.position=pos;

        transform.rotation =rota;

        transform.Rotate(0,angle,0);

    }

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

[AppleScript] 纯文本查看 复制代码

?


01

02

03

04

05

06

07

08

09

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

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

   int number=0; //计数器

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

   {

       int i =number% 2;

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

           ShowWall(walls,true);

       walls =wall;

       if(number>0)

       {

           ShowWall(walls,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.OpenDoor(walls.position,walls.rotation);

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

   }

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

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


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

[AppleScript] 纯文本查看 复制代码

?


01

02

03

04

05

06

07

08

09

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

public classBullet :MonoBehaviour //子弹[/align]

[align=left]{[/align]

[align=left]public float speed;[/align]

[align=left]Rigidbody rig;[/align]

[align=left]DoorManager dm; //传送门管理器[/align]

[align=left]Transform wall;[/align]

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

[align=left]void Start()[/align]

[align=left]{[/align]

[align=left]rig =GetComponent<Rigidbody>();[/align]

[align=left]dm =FindObjectOfType<DoorManager>();[/align]

[align=left]}[/align]

[align=left]void Update()[/align]

[align=left]{[/align]

[align=left]rig.velocity =transform.forward *Time.deltaTime *speed; //前进[/align]

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

[align=left]if(Mathf.Abs(transform.position.z)>8|| Mathf.Abs(transform.position.x)>5)[/align]

[align=left]gameObject.SetActive(false); [/align]

[align=left]}[/align]

[align=left]void OnCollisionEnter(Collision other)//碰撞一次[/align]

[align=left]{[/align]

[align=left]if(other.collider.CompareTag("Wall"))[/align]

[align=left]{[/align]

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

[align=left]if(open&&wall !=other.transform)[/align]

[align=left]{[/align]

[align=left]open=false;[/align]

[align=left]wall =other.transform;[/align]

[align=left]dm.AddWall(other.transform);[/align]

[align=left]Invoke("ColdOpen",0.2f); //0.2秒后完成冷却[/align]

[align=left]}[/align]

[align=left]}[/align]

[align=left]gameObject.SetActive(false); [/align]

[align=left]}[/align]

[align=left]void ColdOpen()[/align]

[align=left]{[/align]

[align=left]open=true;[/align]

[align=left]}[/align]

[align=left]


子弹的特效和准心会根据DoorManager里的计数器单双来决定当前颜色:

传送主角

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


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

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

[AppleScript] 纯文本查看 复制代码

?


01

02

03

04

05

06

07

08

09

10

11

12

13

14

15

16

17

18

19

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.localPosition.x)<0.3f &&Mathf.Abs(substitutes.localPosition.y)<1&&b)

       {

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

           player.position=Cameras[j].position;

           Quaternion r =Cameras[j].rotation;

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

       }

   }

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

传送子弹


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

[AppleScript] 纯文本查看 复制代码

?


01

02

03

04

05

06

07

08

09

10

11

12

13

14

15

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

   {

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

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

       Vector3lp =bullet.localPosition;

       Quaternion lr =bullet.localRotation;

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

       if(wall ==walls[0])

           bullet.parent =walls[1];

       else

           bullet.parent =walls[0];

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

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

       bullet.localRotation =lr;

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

   }

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

[AppleScript] 纯文本查看 复制代码

?


1

2

3

4

5

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

    {

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

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

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

    }

传送门动画


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

[AppleScript] 纯文本查看 复制代码

?


01

02

03

04

05

06

07

08

09

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

Vector3pos; //位置

 Vector3scale;//大小

 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写了一个方法,会根据两个摄像机的位置实时得去调整他们的渲染层级:

[AppleScript] 纯文本查看 复制代码

?


01

02

03

04

05

06

07

08

09

10

11

12

13

14

15

16

17

18

19

20

21

22

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里调用就可以解决刚才的问题了。

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

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

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

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

22.png (87.38 KB, 下载次数: 0)

下载附件 保存到相册

4 小时前 上传

坑点主要就是这两个。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,142评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,298评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,068评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,081评论 1 291
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,099评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,071评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,990评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,832评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,274评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,488评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,649评论 1 347
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,378评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,979评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,625评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,796评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,643评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,545评论 2 352

推荐阅读更多精彩内容