游戏开发论坛

 找回密码
 立即注册
搜索
查看: 3708|回复: 0

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

[复制链接]

1万

主题

1万

帖子

3万

积分

论坛元老

Rank: 8Rank: 8

积分
36572
发表于 2018-11-27 14:06:41 | 显示全部楼层 |阅读模式
1.jpg

文/四五二十

知乎专栏:游戏开发入门指南——Unity+
https://zhuanlan.zhihu.com/gdguide

上篇:用Unity实现传送门效果(一)

大家好。

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

2.jpg

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

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

3.jpg

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

4.jpg

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

  1. <p>public class DoorManager:MonoBehaviour</p><p>
  2. </p><p>{</p><p>
  3. </p><p>public Transform mainCamera;//主摄像机</p><p>
  4. </p><p>public Transform[]substitutes;//替身</p><p>
  5. </p><p>public Transform[]Cameras;//辅助摄像机</p><p>
  6. </p><p>public Door[]doors;//传送门</p>
复制代码

编辑器里将它们拖进去:

5.jpg

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

  1. <p>void SetSubstitutePos()//多层空间摄像机渲染</p><p>
  2. </p><p>{</p><p>
  3. </p><p>//一层空间替身获取主摄像机坐标旋转</p><p>
  4. </p><p>substitutes[0].position=substitutes[1].position=mainCamera.position;</p><p>
  5. </p><p>substitutes[0].rotation=substitutes[1].rotation=mainCamera.rotation;</p><p>
  6. </p><p>//二层空间摄像机获取一层空间替身的本地坐标旋转</p><p>
  7. </p><p>Cameras[1].localPosition=substitutes[0].localPosition;</p><p>
  8. </p><p>Cameras[1].localRotation=substitutes[0].localRotation;</p><p>
  9. </p><p>Cameras[0].localPosition=substitutes[1].localPosition;</p><p>
  10. </p><p>Cameras[0].localRotation=substitutes[1].localRotation;</p><p>
  11. </p><p>//二层空间替身获取二层空间摄像机的坐标旋转</p><p>
  12. </p><p>substitutes[2].position=Cameras[1].position;</p><p>
  13. </p><p>substitutes[2].rotation=Cameras[1].rotation;</p><p>
  14. </p><p>substitutes[3].position=Cameras[0].position;</p><p>
  15. </p><p>substitutes[3].rotation=Cameras[0].rotation;</p><p>
  16. </p><p>//三层空间摄像机获取二层空间替身的本地坐标旋转</p><p>
  17. </p><p>Cameras[2].localPosition=substitutes[3].localPosition;</p><p>
  18. </p><p>Cameras[2].localRotation=substitutes[3].localRotation;</p><p>
  19. </p><p>Cameras[3].localPosition=substitutes[2].localPosition;</p><p>
  20. </p><p>Cameras[3].localRotation=substitutes[2].localRotation;</p><p>
  21. </p><p>}</p>
复制代码

该方法放到LateUpdate里调用。

6.gif
在图中颜色相同线(除白线外)的长度(与传送门的距离)相同,与传送门的夹角也相同,白线表示在那个位置所拥有的物体。

开启传送门

7.jpg

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

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

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

  1. <p>public class Door:MonoBehaviour</p><p>
  2. </p><p>{</p><p>
  3. </p><p>public float angle;//旋转角度</p><p>
  4. </p><p>public void OpenDoor(Vector3 pos,Quaternion rota)//获取位置和旋转打开传送门</p><p>
  5. </p><p>{</p><p>
  6. </p><p>//获取新的位置和旋转</p><p>
  7. </p><p>transform.position=pos;</p><p>
  8. </p><p>transform.rotation=rota;</p><p>
  9. </p><p>transform.Rotate(0,angle,0);</p><p>
  10. </p><p>}</p>
复制代码

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

  1. <p>Transform[]walls=new Transform[2];//保存开门时被隐藏的墙</p><p>
  2. </p><p>int number=0;//计数器</p><p>
  3. </p><p>public void AddWall(Transform wall)//获取当前墙</p><p>
  4. </p><p>{</p><p>
  5. </p><p>int i=number%2;</p><p>
  6. </p><p>if(walls<i>!=null)//不为空则将之前隐藏的先显示</p><p>
  7. </p><p>ShowWall(walls<i>,true);</p><p>
  8. </p><p>walls<i>=wall;</p><p>
  9. </p><p>if(number&gt;0)</p><p>
  10. </p><p>{</p><p>
  11. </p><p>ShowWall(walls<i>,false);//隐藏当前</p><p>
  12. </p><p>if(number==1)</p><p>
  13. </p><p>ShowWall(walls[0],false);</p><p>
  14. </p><p>}</p><p>
  15. </p><p>OpenDoor(i);//打开传送门</p><p>
  16. </p><p>number++;</p><p>
  17. </p><p>}</p><p>
  18. </p><p>void ShowWall(Transform wall,bool b)//隐藏墙</p><p>
  19. </p><p>{</p><p>
  20. </p><p>wall.GetComponent&lt;BoxCollider&gt;().isTrigger=!b;//开关触发器</p><p>
  21. </p><p>wall.GetComponent&lt;SpriteRenderer&gt;().enabled=b;//开关渲染器</p><p>
  22. </p><p>}</p><p>
  23. </p><p>void OpenDoor(int i)//打开传送门</p><p>
  24. </p><p>{</p><p>
  25. </p><p>doors<i>.OpenDoor(walls<i>.position,walls<i>.rotation);</p><p>
  26. </p><p>pm.startColor=i==0?Color.red:Color.blue;</p><p>
  27. </p><p>}</p>
复制代码

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

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

8.gif

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

  1. <p>public class Bullet:MonoBehaviour//子弹</p><p>
  2. </p><p>{</p><p>
  3. </p><p>public float speed;</p><p>
  4. </p><p>Rigidbody rig;</p><p>
  5. </p><p>DoorManager dm;//传送门管理器</p><p>
  6. </p><p>Transform wall;</p><p>
  7. </p><p>bool open=true;//启动传送门也需要冷却时间</p><p>
  8. </p><p>void Start()</p><p>
  9. </p><p>{</p><p>
  10. </p><p>rig=GetComponent&lt;Rigidbody&gt;();</p><p>
  11. </p><p>dm=FindObjectOfType&lt;DoorManager&gt;();</p><p>
  12. </p><p>}</p><p>
  13. </p><p>void Update()</p><p>
  14. </p><p>{</p><p>
  15. </p><p>rig.velocity=transform.forward*Time.deltaTime*speed;//前进</p><p>
  16. </p><p>//飞出地图一定距离自动禁用(地图放在世界中心)</p><p>
  17. </p><p>if(Mathf.Abs(transform.position.z)&gt;8||Mathf.Abs(transform.position.x)&gt;5)</p><p>
  18. </p><p>gameObject.SetActive(false);</p><p>
  19. </p><p>}</p><p>
  20. </p><p>void OnCollisionEnter(Collision other)//碰撞一次</p><p>
  21. </p><p>{</p><p>
  22. </p><p>if(other.collider.CompareTag("Wall"))</p><p>
  23. </p><p>{</p><p>
  24. </p><p>//撞到的墙不是刚才的墙,防止两道门开在同一面墙上</p><p>
  25. </p><p>if(open&&wall!=other.transform)</p><p>
  26. </p><p>{</p><p>
  27. </p><p>open=false;</p><p>
  28. </p><p>wall=other.transform;</p><p>
  29. </p><p>dm.AddWall(other.transform);</p><p>
  30. </p><p>Invoke("ColdOpen",0.2f);//0.2秒后完成冷却</p><p>
  31. </p><p>}</p><p>
  32. </p><p>}</p><p>
  33. </p><p>gameObject.SetActive(false);</p><p>
  34. </p><p>}</p><p>
  35. </p><p>void ColdOpen()</p><p>
  36. </p><p>{</p><p>
  37. </p><p>open=true;</p><p>
  38. </p><p>}</p>
复制代码

9.gif

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

传送主角

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

10.jpg

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

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

  1. <p>void DeliveryPlayer()//传送主角</p><p>
  2. </p><p>{</p><p>
  3. </p><p>if(number&gt;=2)//有两道门后可以执行传送</p><p>
  4. </p><p>{</p><p>
  5. </p><p>DeliveryCondition(0,substitutes[0].localPosition.z&gt;0);</p><p>
  6. </p><p>DeliveryCondition(1,substitutes[1].localPosition.z&lt;0);</p><p>
  7. </p><p>}</p><p>
  8. </p><p>}</p><p>
  9. </p><p>void DeliveryCondition(int i,bool b)//传送主角条件</p><p>
  10. </p><p>{</p><p>
  11. </p><p>int j=Mathf.Abs(i-1);//另一道门的索引</p><p>
  12. </p><p>//判断某个一层替身与父物体(传送门)的位置关系</p><p>
  13. </p><p>if(Mathf.Abs(substitutes<i>.localPosition.x)&lt;0.3f&&Mathf.Abs(substitutes<i>.localPosition.y)&lt;1&&b)</p><p>
  14. </p><p>{</p><p>
  15. </p><p>//将主角传送至另一道门位置</p><p>
  16. </p><p>player.position=Cameras[j].position;</p><p>
  17. </p><p>Quaternion r=Cameras[j].rotation;</p><p>
  18. </p><p>player.rotation=new Quaternion(player.rotation.x,r.y,player.rotation.z,r.w);</p><p>
  19. </p><p>}</p><p>
  20. </p><p>}</p><p></p>
复制代码

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

传送子弹

111.gif

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

  1. <p>public void DeliveryBullet(Transform bullet,Transform wall)//传送子弹</p><p>
  2. </p><p>{</p><p>
  3. </p><p>bullet.parent=wall;//获取该墙成为子弹父物体</p><p>
  4. </p><p>//保存自身本地坐标和旋转</p><p>
  5. </p><p>Vector3 lp=bullet.localPosition;</p><p>
  6. </p><p>Quaternion lr=bullet.localRotation;</p><p>
  7. </p><p>//让另一道门成为子弹父物体</p><p>
  8. </p><p>if(wall==walls[0])</p><p>
  9. </p><p>bullet.parent=walls[1];</p><p>
  10. </p><p>else</p><p>
  11. </p><p>bullet.parent=walls[0];</p><p>
  12. </p><p>//将刚才的本地坐标和旋转再赋予子弹</p><p>
  13. </p><p>bullet.localPosition=new Vector3(-lp.x,lp.y,-lp.z);</p><p>
  14. </p><p>bullet.localRotation=lr;</p><p>
  15. </p><p>bullet.Rotate(0,180,0,Space.World);</p><p>
  16. </p><p>}</p><p>
  17. </p><p>然后在子弹的脚本里使用触发方式调用:</p><p>
  18. </p><p>void OnTriggerEnter(Collider other)//触发一次</p><p>
  19. </p><p>{</p><p>
  20. </p><p>wall=other.transform;//获取被触发的墙</p><p>
  21. </p><p>if(wall!=transform.parent)//触发的物体(墙)不是自己的父物体</p><p>
  22. </p><p>dm.DeliveryBullet(transform,wall);//传送子弹</p><p>
  23. </p><p>}</p><p></p>
复制代码

传送门动画

20181127135802.gif

13.jpg

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

  1. <p>Vector3 pos;//位置</p><p>
  2. </p><p>Vector3 scale;//大小</p><p>
  3. </p><p>public float angle;//旋转角度</p><p>
  4. </p><p>public Transform CopyDoor;//把假门拖进去</p><p>
  5. </p><p>void Start()</p><p>
  6. </p><p>{</p><p>
  7. </p><p>//记录初始位置和大小</p><p>
  8. </p><p>pos=transform.position;</p><p>
  9. </p><p>scale=transform.localScale;</p><p>
  10. </p><p>}</p><p>
  11. </p><p>void Update()</p><p>
  12. </p><p>{</p><p>
  13. </p><p>//位置发生改变</p><p>
  14. </p><p>if(pos!=transform.position)</p><p>
  15. </p><p>{</p><p>
  16. </p><p>//更新位置和旋转信息</p><p>
  17. </p><p>pos=transform.position;</p><p>
  18. </p><p>transform.localScale=Vector3.zero;</p><p>
  19. </p><p>}</p><p>
  20. </p><p>//真门变大动画</p><p>
  21. </p><p>transform.localScale=Vector3.Lerp(transform.localScale,scale,Time.deltaTime*10);</p><p>
  22. </p><p>if(CopyDoor.gameObject.activeInHierarchy)//如果假门被启用调用展示动画</p><p>
  23. </p><p>ShowPrefabDoor();</p><p>
  24. </p><p>}</p><p>
  25. </p><p>void DisplayDoor()//显示假门</p><p>
  26. </p><p>{</p><p>
  27. </p><p>//首先复制真门的位置旋转尺寸,然后启用</p><p>
  28. </p><p>CopyDoor.position=pos;</p><p>
  29. </p><p>CopyDoor.rotation=transform.rotation;</p><p>
  30. </p><p>CopyDoor.localScale=scale;</p><p>
  31. </p><p>CopyDoor.gameObject.SetActive(true);</p><p>
  32. </p><p>}</p><p>
  33. </p><p>void ShowPrefabDoor()//展示假门动画</p><p>
  34. </p><p>{</p><p>
  35. </p><p>//假门变小</p><p>
  36. </p><p>CopyDoor.localScale=Vector3.Lerp(CopyDoor.localScale,Vector3.zero,Time.deltaTime*10);</p><p>
  37. </p><p>if(CopyDoor.localScale.x&lt;0.1f)//小到一定程度就禁用</p><p>
  38. </p><p>CopyDoor.gameObject.SetActive(false);</p><p>
  39. </p><p>}</p>
复制代码

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

14 (1).gif

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

15.jpg

或者

16.jpg

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

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

  1. <p>void SwitchCameraDepth()//切换二层空间摄像机层级</p><p>
  2. </p><p>{</p><p>
  3. </p><p>//如果两个一层替身本地高度差不多,哪个替身离父物体Z轴距离近,一起的摄像机层级越低</p><p>
  4. </p><p>if(Mathf.Abs(substitutes[0].localPosition.y-substitutes[1].localPosition.y)&lt;0.1f)</p><p>
  5. </p><p>{</p><p>
  6. </p><p>if(Mathf.Abs(substitutes[0].localPosition.z)&lt;Mathf.Abs(substitutes[1].localPosition.z))</p><p>
  7. </p><p>SetDepth(Cameras[0],Cameras[1]);</p><p>
  8. </p><p>else</p><p>
  9. </p><p>SetDepth(Cameras[1],Cameras[0]);</p><p>
  10. </p><p>}</p><p>
  11. </p><p>else//如果高度相差很大,哪个替身与父物体Y轴距离小,一起的子物体摄像机层级越低</p><p>
  12. </p><p>{</p><p>
  13. </p><p>if(Mathf.Abs(substitutes[0].localPosition.y)&lt;Mathf.Abs(substitutes[1].localPosition.y))</p><p>
  14. </p><p>SetDepth(Cameras[0],Cameras[1]);</p><p>
  15. </p><p>else</p><p>
  16. </p><p>SetDepth(Cameras[1],Cameras[0]);</p><p>
  17. </p><p>}</p><p>
  18. </p><p>}</p><p>
  19. </p><p>void SetDepth(Transform camera1,Transform camera2)//设置二层空间两个摄像机渲染层级</p><p>
  20. </p><p>{</p><p>
  21. </p><p>camera1.GetComponent&lt;Camera&gt;().depth=-3;</p><p>
  22. </p><p>camera2.GetComponent&lt;Camera&gt;().depth=-2;</p><p>
  23. </p><p>}</p><p></p>
复制代码

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

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

17.jpg

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

18.jpg

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

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

19.jpg

坑点主要就是这两个。

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

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


https://github.com/wushupei/TransmissionGate.gitgithub.com


来,想系统性学习游戏开发、学习Unity开发的,欢迎围观:


http://levelpp.com/


在线的教学视频:


https://space.bilibili.com/38043731/#/


以及QQ交流群:869551769

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

作品发布|文章投稿|广告合作|关于本站|游戏开发论坛 ( 闽ICP备17032699号-3 )

GMT+8, 2024-4-20 11:52

Powered by Discuz! X3.4

Copyright © 2001-2021, Tencent Cloud.

快速回复 返回顶部 返回列表