视差滚动

现在我们已经创建了一个静态场景,还有玩家和敌人。但是依旧很无趣,所以我们该去增强我们的背景和场景了。

有种特效专业出没于各大2D游戏15年,这就是所谓的视差滚动(parallax scrolling)。

简单的说就是,给背景层不同的移动速度(也就是说越远的层移动速度越慢)。如果正确的实践,这种方式会带给玩家一种场景有深度的效果。这确实是一种很酷炫而且比较容易实现的效果。

现在我们开始在Unity里实现它吧。

原理: 定义我们游戏所需滚动

添加一个滚动轴需要思考一下我们如何更好的利用好我们的这个新技能。

最好想清楚了再写代码。:)

我们想移动什么?

我们做一下决定:

  1. 第一选择:玩家和摄像机移动。其余固定。
  2. 第二选择:玩家和摄像机相对是静止的。背景移动,所以就像个跑步机。

第一种选择是不需要用脑子的,如果你有一个Perspective摄像机的话。视差滚动很明显:背景元素有更大的深度。因此它在场景的更后面,也移动得更慢。

但是在Unity里对于一个标准的2D游戏而言,我们使用的是Orthographic摄像机。我们没有关于深度的渲染。

关于摄像机:还记得你的摄像机游戏对象的"Projection"属性么。它在我们的游戏里被设置为Orthographic
如果是Perspective,那就意味着这是一个传统的3D游戏摄像机,带有深度的管理。Orthographic的话渲染所有的东西都以相同深度。这点对于一个游戏中的GUI或者一个2D游戏很有用。

为了给我们的游戏添加视差滚动,我们混合使用这两种选择。我们会有两个滚动视图:

  1. 玩家跟随摄像机向前移动。
  2. 背景元素用不同的速度移动(除了相机移动)。

注意:你可能会问:"为啥我们不设置摄像机为玩家对象的子对象?"。确实是,在Unity中,如果你设置一个对象(摄像机或者其他的)为另一个游戏对象的孩子,这个对象将保持与其父对象的相对位置。所以如果摄像机是一个玩家的孩子, 摄像机会保持跟踪玩家。这也是一种解决方案,但是这对于游戏性来说有些不合适。

在一个shmup游戏里,摄像机会限制玩家的移动。如果摄像机跟随玩家水平或者垂直的移动,那玩家可以随意移动甚至飞出地图。在这里我们只想把玩家限制在我们设定的区域里。

我们建议始终要保持相机在一个2D游戏里独立。即便是一些平台游戏里,摄像机都没有和玩家挂钩: 摄像机根据某些设定条件跟踪玩家。你可以看看超级马里奥游戏里的摄像机,它就完成的不错。

敌军的出生

添加滚动后,我们的游戏出现了其他一些后果,特别是敌人。目前,敌人只是移动来移动去,然后在游戏开始的时候发点子弹。然后我们想让敌人通过出生的方式出现,并且出生前处于无敌状态。

我们怎样生成敌人啦?这完全取决于游戏。你可以定义一些事件,一旦它们被触发就生成敌人。关于出身敌人的位置等信息也需要预定义。

我们这样:我们先把Poulpies放到场景(直接从Prefab拖到场景就行了)。默认的是他们会保持静止,并且处于无敌状态,知道摄像机接触到并激活它们。

好消息是我们通过Unity编辑器来设置敌人。你没有看错,你啥都不需要做,你已经有了一个关卡编辑器了。

再说一次啊,我们只是选择这么做,你也可以选其他的。;)

注意:我们真心觉得使用Unity作为关卡编辑器是性价比非常高的。如果你有时间、有钱以及专用关卡设计师,那就必须要有个专用工具才能配得上的身份了。

平面

首先我们要定义我们所需的平面,指定它们是否循环滚动。一个循环滚动的背景会在关卡执行期间不停的循环滚动。比如,对于天空背景来说,这就很有用。

添加一个新的layer到场景里装背景元素。

我们这样设置:

| 层 | 循环滚动 | 位置 |
| ------ | ---------- | ------- |
| 天空底层背景 | 是 | (0, 0, 10) |
| 底层背景(第一个"飞台") | 否 | (0, 0, 9) |
| 中层背景(第二个"飞台") | 否 | (0, 0, 5) |
| 玩家和敌人所在的前景 | 否 | (0, 0, 0) |

我们可以想象有些层在玩家对象的前面。要让Z轴的值在[0, 10]区间范围之内,否则你就要调整摄像机。

程咬金,当心你的斧子:如果你在前景层添加了层,你要当心它的可见性。很多游戏没有使用这个技术的原因就是它会降低游戏的清晰度。

实践:深入代码

现在我们看看怎样在我们的游戏里实现视差滚动。

Unity在它的标准库里有一些视差滚动脚本(看看在Asset Store里的2D平台游戏demo)。你也可以用这些,但你肯定还是会对怎样构建这些脚本感兴趣的。

标准库:这是一种技巧,但是注意不要滥用它们。使用标准库会阻塞你的想法,同时让你游戏无法出众。它们让玩家一种觉得游戏有种Unity统一风格的味道。
就跟flash游戏一样。。。一种多胞胎克隆人的即视感。

简单滚动

我们先做这个个游戏简单部分:滚动背景图。

还记得我们之前用过的"MoveScript"么?基本上是一样的:速度和方向随着时间被改变。

创建一个"ScrollingScript"脚本:

  1. using UnityEngine;
  2. /// <summary>
  3. /// Parallax scrolling script that should be assigned to a layer
  4. /// </summary>
  5. public class ScrollingScript : MonoBehaviour
  6. {
  7. /// <summary>
  8. /// Scrolling speed
  9. /// </summary>
  10. public Vector2 speed = new Vector2(2, 2);
  11. /// <summary>
  12. /// Moving direction
  13. /// </summary>
  14. public Vector2 direction = new Vector2(-1, 0);
  15. /// <summary>
  16. /// Movement should be applied to camera
  17. /// </summary>
  18. public bool isLinkedToCamera = false;
  19. void Update()
  20. {
  21. // Movement
  22. Vector3 movement = new Vector3(
  23. speed.x * direction.x,
  24. speed.y * direction.y,
  25. 0);
  26. movement *= Time.deltaTime;
  27. transform.Translate(movement);
  28. // Move the camera
  29. if (isLinkedToCamera)
  30. {
  31. Camera.main.transform.Translate(movement);
  32. }
  33. }
  34. }

把这个脚本通过这些值和游戏对象关联起吧:

| 层 | 速度 | 方向 | 连接摄像机 |
| ----- | ------ | ------- | ------------ |
| 0 - Background | (1, 1) | (-1, 0, 0) | 否 |
| 1 - Background elements | (1.5, 1.5) | (-1, 0, 0) | 否 |
| 2 - Middleground | (2.5, 2.5) | (-1, 0, 0) | 否 |
| 3 - Foreground | (1, 1) | (1, 0, 0) | 是 |

为了使场景更加真实,我们还需要添加一些元素到场景里:

  • 添加第三个背景图,之前我们已经添加了两个(请看教程关于添加和显示背景图章节)。
  • 在1 - Background层添加一些的"飞台"元素。
  • 在2 - Middleground层添加一切正常尺寸的"飞台"。
  • 在3 - Foreground层右边添加一些敌人。远离摄像机。

结果图:

还不错。但是我们看到敌人在摄像机范围之外的时候,甚至还没出生的时候就开始移动、射击。

此外,这些敌人和玩家对象擦肩而过后没有再回来(缩小场景视图,你可以看到左边场景里,这些"章鱼"还在移动)。

我们会马上修复这些问题。首先我们要管理这个无限滚动的天空背景。

背景视图无限滚动

为了得到一个无尽的背景,我们只需要观察一下在左侧无尽层的孩子。

当这个对象到达摄像机左侧边缘的时候,我们把它移动到该层的右边。无穷无尽。

对于一个充满图片的层而言,摄像机要覆盖尽可能小的范围。天空的三部分,完全是任意的。

你要找到一个游戏资源消耗和游戏灵活性的平衡点。

在我们的例子中,我们得到层的所有孩子节点,检查它们的渲染器。

使用渲染器组件注意:这种方法对于不可见的对象没用(比如处理脚本的)。当然,你也几乎不会对这种对象去运用。

我们将使用一个方便的方法来检查对象的渲染器是否是对相机可见。它既不是类也不是脚本,是C#的extension。

Extension: C#语言可以扩展一个类,而无需看到类的源代码。
创建一个静态方法,第一个参数跟着需要拓展的类型和它的一个实例。现在你的这个类就自动在使用这个类的地方拥有了这个方法。

脚本"RendererExtensions"

创建一个新的C#文件命名为"RendererExtensions.cs", 写入以下代码:

  1. using UnityEngine;
  2. public static class RendererExtensions
  3. {
  4. public static bool IsVisibleFrom(this Renderer renderer, Camera camera)
  5. {
  6. Plane[] planes = GeometryUtility.CalculateFrustumPlanes(camera);
  7. return GeometryUtility.TestPlanesAABB(planes, renderer.bounds);
  8. }
  9. }

很简单,不是么?

命名空间: 你可能已经注意到了Unity当你从"Project"视图创建一个MonoBehaviour脚本的时候,外围并没有被namespace关键字包裹。目前,Unity确实有处理处理命名空间。
但是在本教程中,我们没有使用命名空间。当然在实际的项目中,你可能会考虑用到它们。或者是给自己的类添加比较特别的前缀,避免和其他的第三方库冲突(比如NGUI)。

我们将在无尽层最左边的对象上调用这个方法。

完整"ScrollingScript"

  1. using System.Collections.Generic;
  2. using System.Linq;
  3. using UnityEngine;
  4. /// <summary>
  5. /// Parallax scrolling script that should be assigned to a layer
  6. /// </summary>
  7. public class ScrollingScript : MonoBehaviour
  8. {
  9. /// <summary>
  10. /// Scrolling speed
  11. /// </summary>
  12. public Vector2 speed = new Vector2(10, 10);
  13. /// <summary>
  14. /// Moving direction
  15. /// </summary>
  16. public Vector2 direction = new Vector2(-1, 0);
  17. /// <summary>
  18. /// Movement should be applied to camera
  19. /// </summary>
  20. public bool isLinkedToCamera = false;
  21. /// <summary>
  22. /// 1 - Background is infinite
  23. /// </summary>
  24. public bool isLooping = false;
  25. /// <summary>
  26. /// 2 - List of children with a renderer.
  27. /// </summary>
  28. private List<Transform> backgroundPart;
  29. // 3 - Get all the children
  30. void Start()
  31. {
  32. // For infinite background only
  33. if (isLooping)
  34. {
  35. // Get all the children of the layer with a renderer
  36. backgroundPart = new List<Transform>();
  37. for (int i = 0; i < transform.childCount; i++)
  38. {
  39. Transform child = transform.GetChild(i);
  40. // Add only the visible children
  41. if (child.renderer != null)
  42. {
  43. backgroundPart.Add(child);
  44. }
  45. }
  46. // Sort by position.
  47. // Note: Get the children from left to right.
  48. // We would need to add a few conditions to handle
  49. // all the possible scrolling directions.
  50. backgroundPart = backgroundPart.OrderBy(
  51. t => t.position.x
  52. ).ToList();
  53. }
  54. }
  55. void Update()
  56. {
  57. // Movement
  58. Vector3 movement = new Vector3(
  59. speed.x * direction.x,
  60. speed.y * direction.y,
  61. 0);
  62. movement *= Time.deltaTime;
  63. transform.Translate(movement);
  64. // Move the camera
  65. if (isLinkedToCamera)
  66. {
  67. Camera.main.transform.Translate(movement);
  68. }
  69. // 4 - Loop
  70. if (isLooping)
  71. {
  72. // Get the first object.
  73. // The list is ordered from left (x position) to right.
  74. Transform firstChild = backgroundPart.FirstOrDefault();
  75. if (firstChild != null)
  76. {
  77. // Check if the child is already (partly) before the camera.
  78. // We test the position first because the IsVisibleFrom
  79. // method is a bit heavier to execute.
  80. if (firstChild.position.x < Camera.main.transform.position.x)
  81. {
  82. // If the child is already on the left of the camera,
  83. // we test if it's completely outside and needs to be
  84. // recycled.
  85. if (firstChild.renderer.IsVisibleFrom(Camera.main) == false)
  86. {
  87. // Get the last child position.
  88. Transform lastChild = backgroundPart.LastOrDefault();
  89. Vector3 lastPosition = lastChild.transform.position;
  90. Vector3 lastSize = (lastChild.renderer.bounds.max - lastChild.renderer.bounds.min);
  91. // Set the position of the recyled one to be AFTER
  92. // the last child.
  93. // Note: Only work for horizontal scrolling currently.
  94. firstChild.position = new Vector3(lastPosition.x + lastSize.x, firstChild.position.y, firstChild.position.z);
  95. // Set the recycled child to the last position
  96. // of the backgroundPart list.
  97. backgroundPart.Remove(firstChild);
  98. backgroundPart.Add(firstChild);
  99. }
  100. }
  101. }
  102. }
  103. }
  104. }

(以上注释中的数字对应以下说明)

解释

  1. 我们需要一个公有变量在"Inspector"来开关循环。
  2. 我们还需要一个私有变量存储层的子节点。
  3. Start()方法中, 我们把拥有渲染器的子节点保存到backgroundPart。多亏了LINQ, 我们通过它们的X坐标来排序,把越左边的放到数组的前面。
  4. Update()方法中, 如果isLooping被设置成true, 我们取回backgroundPart里第一个子节点。我们看它是不是完全在摄像机范围外,如果它在范围外,我们把它位置调整到最后一个(最右边的)子节点的后面。当然我们还要同步更新到backgroundPart列表里。

某种意义上说,backgroundPart代表了场景的情况。

记得要在0 - Background的"Inspector"面板里给"ScrollingScript"的"Is Looping"属性启用。否则它肯定没办法正常运行。

(点击本图观看动画)

噢,yes!我们现在已经实现了视差滚动的功能了。

注意:为什么我们不使用OnBecameVisible()OnBecameInvisible()方法啦?

这些方法的基本思路都是在对象被渲染的时候执行一个代码段(反之亦然)。他们就像Start()或者Stop()方法(如果你需要,直接在MonoBehaviour添加方法,Unity会直接使用它)。

问题是这些方法在被Unity编辑器的场景视图渲染的时候也会被调用。这意味着我们在Unity编辑器里和最终编译平台得到的效果会不一样。这不仅危险而且很可笑。我们强烈推荐不要使用这些方法。

奖励: 增强现有脚本

让我们更新之前的脚本。

出生的敌人v2.0版

我们之前说过,敌人在摄像机可以看到之前是被禁用了的。

一旦他们离开屏幕也要被移除。

我们需要更新"EnemyScript"脚本, 实现以下功能:

  1. 禁止移动,碰撞和自动开火(初始化的时候)。
  2. 检查摄像机范围的渲染器。
  3. 激活自身。
  4. 当它们处于相机范围之外的时候摧毁游戏对象。

(数字对应代码中的注释中的数字)

  1. using UnityEngine;
  2. /// <summary>
  3. /// Enemy generic behavior
  4. /// </summary>
  5. public class EnemyScript : MonoBehaviour
  6. {
  7. private bool hasSpawn;
  8. private MoveScript moveScript;
  9. private WeaponScript[] weapons;
  10. void Awake()
  11. {
  12. // Retrieve the weapon only once
  13. weapons = GetComponentsInChildren<WeaponScript>();
  14. // Retrieve scripts to disable when not spawn
  15. moveScript = GetComponent<MoveScript>();
  16. }
  17. // 1 - Disable everything
  18. void Start()
  19. {
  20. hasSpawn = false;
  21. // Disable everything
  22. // -- collider
  23. collider2D.enabled = false;
  24. // -- Moving
  25. moveScript.enabled = false;
  26. // -- Shooting
  27. foreach (WeaponScript weapon in weapons)
  28. {
  29. weapon.enabled = false;
  30. }
  31. }
  32. void Update()
  33. {
  34. // 2 - Check if the enemy has spawned.
  35. if (hasSpawn == false)
  36. {
  37. if (renderer.IsVisibleFrom(Camera.main))
  38. {
  39. Spawn();
  40. }
  41. }
  42. else
  43. {
  44. // Auto-fire
  45. foreach (WeaponScript weapon in weapons)
  46. {
  47. if (weapon != null && weapon.enabled && weapon.CanAttack)
  48. {
  49. weapon.Attack(true);
  50. }
  51. }
  52. // 4 - Out of the camera ? Destroy the game object.
  53. if (renderer.IsVisibleFrom(Camera.main) == false)
  54. {
  55. Destroy(gameObject);
  56. }
  57. }
  58. }
  59. // 3 - Activate itself.
  60. private void Spawn()
  61. {
  62. hasSpawn = true;
  63. // Enable everything
  64. // -- Collider
  65. collider2D.enabled = true;
  66. // -- Moving
  67. moveScript.enabled = true;
  68. // -- Shooting
  69. foreach (WeaponScript weapon in weapons)
  70. {
  71. weapon.enabled = true;
  72. }
  73. }
  74. }

开始游戏。。。额,好吧,这有个bug。

禁用"MoveScript"产生了一个副作用:玩家对象始终接触不到敌人,它们都跟随3 - Foreground层一起滚动了:

记得我们添加到"ScrollingScript"这层是为了让摄像机跟随玩家对象移动吧。

所以这里有个简单的解决方案:从3 - Foreground移动"ScrollingScript"到玩家对象身上!

这个层里唯一需要移动的就是玩家对象,这个脚本也没有指定与特定对象。

按下"Play"按钮:起作用了!

  1. 敌人们在被摄像机可见之前都被禁用了。
  2. 当它们处于摄像机之外的时候都消失了。

(点击图片查看详情)

限制玩家在摄像机范围之类

你可能已经注意到了,玩家没有被限制在摄像机范围之内。如果在运行游戏中,一直按着左键,你就会看到玩家对象离开了摄像机范围了。

我们得解决这个问题。

打开"PlayerScript", 在"Update()"方法后面添加:

  1. void Update()
  2. {
  3. // ...
  4. // 6 - Make sure we are not outside the camera bounds
  5. var dist = (transform.position - Camera.main.transform.position).z;
  6. var leftBorder = Camera.main.ViewportToWorldPoint(
  7. new Vector3(0, 0, dist)
  8. ).x;
  9. var rightBorder = Camera.main.ViewportToWorldPoint(
  10. new Vector3(1, 0, dist)
  11. ).x;
  12. var topBorder = Camera.main.ViewportToWorldPoint(
  13. new Vector3(0, 0, dist)
  14. ).y;
  15. var bottomBorder = Camera.main.ViewportToWorldPoint(
  16. new Vector3(0, 1, dist)
  17. ).y;
  18. transform.position = new Vector3(
  19. Mathf.Clamp(transform.position.x, leftBorder, rightBorder),
  20. Mathf.Clamp(transform.position.y, topBorder, bottomBorder),
  21. transform.position.z
  22. );
  23. // End of the update method
  24. }

在上面的代码里,我们取得摄像机的边界,保证玩家的位置(sprite的中心点)在这个边界之内。

下一步

我们有了一个滚动的射击者了。

我们刚刚学会了如何给我们的游戏添加一个滚动机制,同时也为背景层增加了视差效果。然后当前的代码只保证了从右向左滚动。接下来我们学着增强它的功能,让它在所有方向滚动。

我们的游戏还需要一些改进来改变游戏性。比如:

  • 减少sprite尺寸
  • 调整速度
  • 增加更多的敌人
  • 让它更有趣

我们将在接下来的章节解决上面说到的问题来调整我们的游戏性。

同时,我们也会关注如何让我们的游戏更加酷炫。通过使用粒子效果。

转#Unity2D范例-6相关推荐

  1. 转#Unity2D范例-7

    玩玩粒子效果 我们的发射的子弹,弹道是很飘逸,但是我们也该给它加点粒子效果来增强它们的视觉效果了. 粒子算是一种很小的sprite,它通过短时间的重复的生成和显示来生成一种酷炫的视觉效果. 我们想想爆 ...

  2. php文本计数器源码,php 简单文本计数器[基于文件系统的页面计数器范例]

    我们的计数器经常会用到文本文件来实现,定义计数器写入的文件是当前目录下count.txt,然后我们应当测试该文件能否打开 基于文件系统的页面计数器范例 $countfile = "num.t ...

  3. Unity2D游戏开发和C#编程大师班

    本课程采用现代游戏开发的最新内容和最新技术(Unity 2D 2022) 学习任何东西的最好方法是以一种真正有趣的方式去做,这就是这门课程的来源.如果你想了解你看到的这些不可思议的游戏是如何制作的,没 ...

  4. 文件读写io操作范例

    系统io读写,copy int main(int argc, char **argv) {  if(argc != 3) {   printf("Usage: %s <src> ...

  5. [IoC容器Unity]第四回:使用范例

    1.引言 前面几个章节介绍了Unity的基本使用,主要分为程序和配置文件两种方法的使用,可以参考一下链接, [IoC容器Unity]第一回:Unity预览 [IoC容器Unity]第二回:Lifeti ...

  6. AJAX范例大搜罗(转载)

    1.每天一个AJAX 该网站提供了很多非常酷的AJAX例子,号称是每天更新一个. 网址:http://www.ajaxcompilation.com/ 2.210个AJAX框架 一个不错的提供Ajax ...

  7. [Android]ViewSwitcher使用范例

    前言 虽然ViewSwitcher的中文API早已翻译出来,但一直没有在项目中使用过,也没有搜到很合适很简单的中文例子,这里与大家一起探讨和分享一下其用法. 声明 欢迎转载,但请保留文章原始出处:) ...

  8. 一步一步学习ASP.NET MVC 1.0创建NerdDinner 范例程序 - 强烈推荐!!!

    一步一步学习ASP.NET MVC 1.0创建NerdDinner 范例程序 本文根据<Professional ASP.NET MVC 1.0>中微软牛人Scott Guthrie 提供 ...

  9. 计算机 微课 论文,探析毕业论文怎么写 关于微课和电脑论文范例30000字

    <微课在中职计算机Flash课程中的应用探析> 该文是关于微课和电脑方面毕业论文怎么写和探析有关论文范例. 谢菲 [摘 要]作为一种新型的教学手段,微课时间短.容量小.内容精,对中职计算机 ...

最新文章

  1. Flex DataGrid可编辑对象实现Enter跳转
  2. CentOS在安装配置 Ngnix_tomcat_PHP_Mysql
  3. Kubernetes Controller Manager 工作原理
  4. optee系统服务/service的实现方式
  5. postgis 导出 栅格_postgis常见的空间数据的导入导出
  6. 15 WM配置-主数据-定义存储区标识符(Storage Section Indicators)
  7. AD20元件重叠绿色报错的解决方法,距离太近绿色报错
  8. Spring 3.x jar 包详解 与 依赖关系
  9. Linux学习之源码2:start_kernel流程
  10. 词组能够进入_四六级翻译100个常考词组~
  11. 修改删除idea快捷键
  12. PCHunter_32X64_2022_03最新版
  13. Ansys电机控制系统分析
  14. Linux中 ll 和 ls 区别
  15. 组件化与插件化的差别在哪里?醍醐灌顶!
  16. PHP 图片木马隐写方法及靶机演示
  17. leetcode刷题(三)——容斥原理
  18. 简单c语言实现小猫钓鱼
  19. 线性表进阶___约瑟夫环问题
  20. 推荐系统10——评分预测问题

热门文章

  1. 【阅读】读书只是生活方式的一种
  2. ajax局部刷新 php,PHP中ajax的局部刷新
  3. 山东春秀高考计算机本科录取率,山东2020年高考录取人数及录取率
  4. 关于制作项目的小插曲
  5. DxLib 下载页,简单翻译(我不懂日文,瞎编的)
  6. Python逃生游戏
  7. 开启 mcrypt php,开启 Mcrypt PHP 扩展
  8. First Happy Work :)
  9. 论文翻译-Recommendation Unlearning
  10. 计算机boss是什么东西,Boss. 是什么意思?