当前位置:   article > 正文

Unity 3D脚本编程与游戏开发(1.7)_unity3d脚本编程与游戏开发12章

unity3d脚本编程与游戏开发12章
2.4.2 常⻅的事件⽅法

        MonoBehaviour的事件⾮常多,官⽅⽂档中共列举了64个。下⾯将其中较为常⻅的⼏⼗个事件按逻辑分类,并列举出来,如表2-6所⽰。
                                                        表2-6 常⻅的事件⽅法
        读者可以浏览各种事件函数,⼤致了解引擎提供的各种事件,⽅便未来实践时使⽤。

2.4.3 实例:跟随主⾓的摄像机

        在第1章制作的3D滚球跑酷游戏⾥,已经实现了让摄像机跟随主⾓⼩球移动的功能。直接把摄像机作为⼩球的⼦物体,虽然是⼀种较⽅便的做法,但是也有很⼤缺陷,如⼩球旋转时摄像机也会跟着旋转。⽽让摄像机跟随⼩球移动最好的⽅法是让摄像机受脚本控制单独运动,⽽不是作为⼦物体直接受其他物体控制。制作跟随物体平移的摄像机,步骤如下。
01 新建脚本FollowCam,并将它挂载到Main Camera(主摄像机)上。
02 编辑脚本代码如下。

  1. using UnityEngine;
  2. public class FollowCam : MonoBehaviour
  3. {
  4. // 追踪的⽬标,在编辑器⾥指定
  5. public Transform followTarget;
  6. Vector3 offset;
  7. void Start()
  8. {
  9. // 算出从⽬标到摄像机的向量,作为摄像机的偏移量
  10. offset = transform.position - followTarget.position;
  11. }
  12. void LateUpdate()
  13. {
  14. // 每帧更新摄像机的位置
  15. transform.position = followTarget.position + offset;
  16. }
  17. }

03 回到Unity编辑器,给脚本指定追踪的⽬标即可,如图2-30所⽰。
        跟随摄像机的脚本有很强的通⽤性,可以⽤在任意游戏⾥。例如,可以把它应⽤在第1章制作的3D滚球跑酷游戏⾥,需要修改的地⽅只有以下3点。
(1)将FollowCam脚本挂载到主摄像机上。
(2)拖曳主⾓⼩球到脚本组件的Follow Target参数上。
(3)拖曳Hierarchy窗⼝⾥的主摄像机标签,使其不再作为⼩球的⼦物体,⽽是作为独⽴物体。
        再次运⾏游戏,可以看到效果和以前⼏乎没有区别。虽然效果相似,但是原理已经⼤不相同了。最后,注意观察在脚本代码中,特意将摄像机位置的更新写在LateUpdate()⽅法中,⽽不是Update()⽅法中。这是为了确保在主⾓移动之后再移动摄像机,避免摄像机在主⾓之前更新位置,但这样可能会造成画⾯运动不平滑的效果。

2.4.4 触发器事件

        在第1章的实例⾥,笔者在设计游戏时使⽤了尽可能少的Unity功能,但是在制作⼩游戏时会发现⼀点——很难避免使⽤触发器。如果没有触发器,就需要⽤⼤量数学运算来检测物体之间的碰撞。本节将专门介绍与触发器有关的3个事件:OnTriggerEnter、OnTriggerStay和OnTriggerExit。
演⽰⼯程很简单,在默认场景中创建⼀个⽴⽅体和⼀个球体,如图2-31所⽰。

接下来的操作步骤如下。
        01 假设⼩球是运动的,且已经有了碰撞体组件。给⼩球添加Rigidbody组件,并勾选刚体的Is Kinematic选项。
        02 假设⽴⽅体表⽰⼀个静⽌的范围,勾选⽴⽅体的Box Collider中的Is Trigger选项,将它变成⼀个触发器。
        03 如果需要⼀个透明但有触发效果的范围,可以禁⽤⽴⽅体的MeshRenderer组件。
        04 创建脚本TestTrigger,并将其挂载到⽴⽅体上,其内容如下。

⼩提⽰
必须给运动的碰撞体加上刚体组件
要让两个物体之间产⽣触发器事件或者碰撞事件,就要求其中⼀
个物体必须带有刚体组件(可以是动⼒学刚体)。如果两个物体均不
含刚体组件,那么就不会触发物理事件。
那么在两个物体中,应该给哪⼀个物体挂载刚体组件呢?答案是
应当给运动的物体挂载刚体组件(可以是动⼒学刚体)。这些规定背
后的原因,会在第3章物理系统中详细讲解。
  1. using UnityEngine;
  2. public class TestTrigger : MonoBehaviour
  3. {
  4. private void OnTriggerEnter(Collider other)
  5. {
  6. Debug.Log("---- 碰撞! " + other.name);
  7. }
  8. private void OnTriggerStay(Collider other)
  9. {Debug.Log("---- 碰撞持续中…… " + Time.time);
  10. }
  11. private void OnTriggerExit(Collider other)
  12. {
  13. Debug.Log("==== 碰撞结束 " + other.name);
  14. }
  15. }

        这样⼀个简单的测试脚本⾜可以体现这3个触发事件的含义了。为简单起⻅,运⾏游戏后,在场景窗⼝中将球体直接移动到⽴⽅体范围内,然后再远离,就会依次触发这3个事件。其在Console窗⼝⾥输出的内容如图2-32所⽰。

        如果仔细观察,会发现输出信息的时间间隔是0.02秒,这正是默认的FixedUpdate事件的时间间隔。这⼀时间间隔的含义也会在之后的第3章物理系统中详细讲解。触发器是制作各种游戏时常⽤的功能,除了在编辑器⾥设置,编写脚本时也总是会⽤到与触发器相关的3个事件。

2.5 协程⼊门

        之前提到,定时创建或销毁物体,可以使⽤Invoke⽅法。但是通过第2.3.5⼩节延迟创建多个物体的代码可以看出,⼤量使⽤Invoke()⽅法的代码⽐较难编写,⽽且难以理解。实际上,Unity已经提供了“协程”这⼀概念,专门处理复杂的定时逻辑。协程的原理有点复杂,本节仅解释它的⼤致⽤法,让读者通过简单的例⼦先将协程技术运⽤起来,在后⽂会进⼀步详细讲解协程的概念。下⾯的代码实现了⼀个简单的计时器,每隔2秒就会在Console窗⼝中显⽰当前游戏经历的时间。

  1. using UnityEngine;
  2. public class TestCoroutine : MonoBehaviour
  3. {
  4. void Start()
  5. {
  6. // 开启⼀个协程,协程函数为Timer
  7. StartCoroutine(Timer());
  8. }
  9. // 协程函数
  10. IEnumerator Timer()
  11. {
  12. // 不断循环执⾏,但是并不会导致死循环
  13. while (true)
  14. {
  15. // 打印4个汉字
  16. Debug.Log(" 测试协程 ");
  17. // 等待1秒
  18. yield return new WaitForSeconds(1);// 打印当前游戏经历的时间
  19. Debug.Log(Time.time);
  20. // 再等待1秒
  21. yield return new WaitForSeconds(1);
  22. }
  23. }
  24. }

执⾏效果如图2-33所⽰。

        这⾥对以上代码做⼀个简单的解释。StartCoroutine⽅法开启了⼀个新的协程函数Timer(),这个协程函数返回值必须是IEnumerator。Timer函数中由于while(true)的存在,会永远运⾏下去。Timer()函数每当运⾏到yield return语句,就会暂时休息,⽽new WaitForSeconds(1)会控制休息的
时间为1秒,1秒后⼜接着执⾏后⾯的内容。
        换个⾓度看Timer()函数,它创造了⼀个优雅的、可以⽅便地控制执⾏时间的程序结构,不再需要使⽤Invoke()那种烦琐的延迟调⽤⽅法。任何需要定时执⾏的逻辑都可以通过在循环体中添加代码,或是再添加⼀个新的协程函数来实现。不必担⼼开设过多协程对效率的影响,只要不在协程函数中做很复杂的操作,那么创建协程本⾝对运⾏效率的影响⾮常有限。有了协程,就可以重写第2.3.5⼩节⾥⾯的例⼦,其代码如下。

  1. using UnityEngine;
  2. public class CoroutineCreate : MonoBehaviour
  3. {
  4. public GameObject prefab;
  5. void Start()
  6. {
  7. StartCoroutine(CreateObject());
  8. }
  9. // 协程函数
  10. IEnumerator CreateObject()
  11. {
  12. for (int i=0; i<10; i++)
  13. {
  14. Vector3 pos = new Vector3(Mathf.Cos(i * (2 *
  15. Mathf.PI) / 10), 0, Mathf.
  16. Sin(i * (2 * Mathf.PI) / 10));
  17. pos *= 5; // 圆环半径是5
  18. Instantiate(prefab, pos, Quaternion.identity);
  19. // 等待0.5秒
  20. yield return new WaitForSeconds(0.5f);
  21. // 0.5秒之后继续执⾏
  22. }
  23. }
  24. }

        运⾏结果与第2.3.5⼩节⼀致,都是每隔0.5秒创建⼀个物体,物体围成环形,但是此处代码简单许多。

2.6 实例:3D射击游戏

        接下来做⼀款简易的俯视⾓度的射击游戏,作为本章的实践项⽬,如图2-34所⽰。

2.6.1 游戏总体设计

        本游戏是⼀个俯视⾓度的射击游戏。玩家从侧上⽅俯视整个场景,主⾓始终位于屏幕中⼼位置。其具体玩法描述如下。
(1)完全使⽤键盘控制,由W、A、S、D键控制⾓⾊的⽅向移动,J键控制射击。(这样做主要是为了简化游戏输⼊部分的逻辑。)
(2)玩家具有多种武器,如⼿枪、霰弹枪和⾃动步枪,每种武器可以按Q键切换。
(3)场景上除了玩家⾓⾊还有若⼲敌⼈。敌⼈会向玩家⽅向移动并射击玩家。
(4)玩家⾓⾊和敌⼈都有⽣命值,中弹后⽣命值减少,减为零时则死亡。

2.6.2 游戏的实现要点

        在游戏实现上,尽可能只使⽤本章提到过的功能,不使⽤其他⾼级功能,这样也能做出具有⼀定可玩性的游戏。下⾯将列举⼀些功能点。
1. 主角脚本
为⽅便起⻅,把键盘控制和主⾓⾏为编写在同⼀个Player脚本⾥。主⾓的移动使⽤Input.GetAxis实现。

2. 跟随式摄像机
为了有更好的灵活性,编写⼀个FollowCam脚本,可以⽤于跟踪场景中任意物体。
3. 武器系统
多种武器具有不同的射击逻辑,如能否持续发射、连续发射时间间隔、⼀次发射⼏颗⼦弹等均有区别,因此要把武器系统单独编写在⼀个脚本组件⾥。
4. 发射⼦弹
⼦弹的实现只需要两个步骤,⾸先由武器创建⼦弹,其次⼦弹的⻜⾏逻辑由⼦弹⾃⾝的脚本所控制。
5. 游戏全局管理器——GameMode
        某些数据是全游戏唯⼀的,如玩家杀敌数量。这种数据最好保存在全游戏唯⼀的对象上。因此要特别创建GameMode,专门⽤来保存全局数据。另外它还负责整体游戏的逻辑,如刷新UI界⾯。
6. 敌⼈移动和射击的实现
敌⼈的移动看起来和玩家⼏乎⼀样,但最重要的区别是,敌⼈的移动不是由键盘触发的,⽽是敌⼈⾃发地进⾏移动。因此需要给敌⼈编写⼀点简单的“智能”逻辑。

2.6.3 创建主角

先简单搭建场景,再创建主⾓。
01 创建⼀个蓝灰⾊平⾯作为地板,位置归0,且放⼤到4倍左右。
02 创建⼀个3D㬵囊体(Capsule),命名为Player,适当调整位置,让它站在地板中间,如图2-35所⽰。

03 主⾓要有⼀个红⾊的脸以表⽰正⾯。给㬵囊体添加⼀个⽴⽅体⼦物体,⼤⼩设置为0.5倍,并将⽴⽅体改为红⾊材质,如图2-36所⽰。

  1. 04 创建脚本Player以控制主⾓的移动。其脚本内容如下。
  2. using UnityEngine;
  3. public class Player : MonoBehaviour
  4. {
  5. // 移动速度
  6. public float speed = 3;
  7. // 最⼤⾎量
  8. public float maxHp = 20;// 变量,输⼊⽅向⽤
  9. Vector3 input;
  10. // 是否死亡
  11. bool dead = false;
  12. // 当前⾎量
  13. float hp;
  14. void Start()
  15. {
  16. // 初始确保满⾎状态
  17. hp = maxHp;
  18. }
  19. void Update()
  20. {
  21. // 将键盘的横向、纵向输⼊,保存在input变量中
  22. input = new Vector3(Input.GetAxis("Horizontal"), 0,
  23. Input.GetAxis("Vertical"));
  24. // 未死亡则执⾏移动逻辑
  25. if (!dead)
  26. {
  27. Move();
  28. }
  29. }
  30. void Move()
  31. {
  32. // 先归⼀化输⼊向量,让输⼊更直接,同时避免斜向移动时速度超过最⼤
  33. 速度
  34. input = input.normalized;
  35. transform.position += input * speed * Time.deltaTime;
  36. // 令⾓⾊前⽅与移动⽅向⼀致
  37. if (input.magnitude > 0.1f)
  38. {
  39. transform.forward = input;
  40. }
  41. // 以上移动⽅式没有考虑阻挡,因此使⽤下⾯的代码限制移动范围
  42. Vector3 temp = transform.position;
  43. const float BORDER = 20;
  44. if (temp.z > BORDER) { temp.z = BORDER; }
  45. if (temp.z < -BORDER) { temp.z = -BORDER; }
  46. if (temp.x > BORDER) { temp.x = BORDER; }
  47. if (temp.x < -BORDER) { temp.x = -BORDER; }
  48. transform.position = temp;
  49. }
  50. }

        05 运⾏游戏,简单测试。通过W、A、S、D键或⽅向键可以控制主⾓的移动,主⾓移动时⾯朝移动⽅向,且移动到地板边缘时⽆法继续移动。如果发现地板⼤⼩与移动范围不⼀致,修改脚本中的BORDER参数和地板⼤⼩即可。由于3D平⾯默认为10⽶宽,因此BORDER参数乘以2再除以10就等于地板的缩放⽐例。
        读者这时可能会发现,⼈物的移动有⼀种“延迟感”,当松开按键的时候,⼈物还会继续⾏⾛⼀⼩段距离。这是因为GetAxis()函数在处理输⼊时故意设计成模拟⼿柄摇杆操作导致的。要修正这个问题,可以把GetAxis()函数改为GetButton()函数,或者通过修改⼯程设置解决。修改⼯程设置的⽅法如下。
        01 选择主菜单Edit→Project Settings,打开Project Settings窗⼝,如图2-37所⽰。

        02 在Project Settings窗⼝左侧的纵向列表中选择Input选项,展开多层嵌套的⼩三⾓形,找到Horizontal和Vertical两个输⼊项,它们分别代表横、纵输⼊轴,其内容是类似的。最后将它们的Gravity(回中⼒度)与Sensitivity(敏感度)的值改为100,如图2-38所⽰。

修改完成后,就可以去掉⽅向键的“延迟感”,横向输⼊和纵向输⼊会很快地在0与1之间切换,⽽不会有从0逐渐增加到0.5,再增加到1.0的过程。
 

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/我家小花儿/article/detail/105925
推荐阅读
相关标签
  

闽ICP备14008679号