七、全力以赴——物理学和 2D 相机

在前一章中,您学习了移动设备的特殊功能以及如何创建触摸和倾斜控制。我们还创建了一个猴球游戏来使用这些新控件。球的转向是通过倾斜装置和触摸屏幕收集香蕉来完成的。我们还通过创建计时器和终点线给了它一些输赢条件。

在这一章中,我们将从猴球游戏中短暂休息一下,探索 Unity 的物理引擎。我们还将了解创建 2D 游戏体验的可用选项。为了做到这一切,我们将重现市场上最受欢迎的手机游戏之一愤怒的小鸟。我们会用物理学来扔鸟和摧毁建筑。我们还将看一下级别选择屏幕的创建。

在本章中,我们将涵盖以下主题:

  • 统一物理学
  • 视差滚动
  • 2D 管道
  • 级别选择

我们将为这一章创建一个新项目,所以启动 Unity,让我们开始吧!

3D 世界中的 2D 游戏

也许在开发游戏时最鲜为人知的事情是,在 3D 游戏引擎中创建 2D 风格的游戏是可能的,比如 Unity。和其他东西一样,它有自己的一套优点和缺点,但是为了产生令人愉快的游戏体验,这个选择是非常值得的。最重要的优势是你可以在游戏中使用 3D 素材。这使得动态照明和阴影很容易包含在内。但是,当使用 2D 引擎时,任何阴影都需要直接绘制到素材中,您将很难使其动态化。不利的一面是在 3D 世界中使用 2D 素材。使用它们是可能的,但是大的文件大小对于实现期望的细节和以防止它看起来像素化是必要的。然而,大多数 2D 引擎使用矢量艺术,这将保持图像的线条平滑,因为它是按比例放大和缩小。此外,可以对三维素材使用普通动画,但任何 2D 素材通常都需要逐帧动画。总的来说,对许多开发者来说,优势已经超过了劣势,创造了大量好看的 2D 游戏,你可能永远不会意识到这些游戏实际上是用 3D 游戏引擎制作的。

为了满足开发人员对 2D 游戏支持日益增长的需求,Unity 团队一直在为 3D 引擎创建一个优化的 2D 管道。创建项目时,您可以选择 2D 默认值,优化素材以用于 2D 游戏。虽然 Unity 仍然没有直接的矢量图形支持,但许多其他功能已经过优化,可以在 2D 世界中更好地工作。其中一个最大的特点是物理引擎的 2D 优化,这将是我们在这一章的重点。我们将使用的所有原理都将转移到 3D 物理,这将在设置和使用它时节省一些麻烦。

设置开发环境

为了探索如何在一个主要是 3D 的引擎中制作一个 2D 游戏,以及物理的使用,我们将重新制作一个非常受欢迎的 2D 游戏,愤怒的小鸟。然而,在我们能够深入到游戏的核心之前,我们需要设置我们的开发环境,以便我们为 2D 游戏创作进行优化。让我们使用以下步骤来实现这一点:

  1. 首先,我们需要在 Unity 中创建新项目。将其命名为Ch7_AngryBirds会很有效。我们还需要在模板下选择 2D ,所以所有的默认值都是为我们的 2D 游戏设置的。
  2. 我们还需要确保将构建设置字段中的目标平台更改为安卓,并将捆绑包标识符设置为适当的值。我们不想以后再担心这个。
  3. There are a few differences that you will notice right away. First, you can only pan from side to side and up and down when moving around in the scene. This is a setting that can be toggled in the top-middle of the Scene view, by clicking on the little 2D button. Also, if you select the camera in the Hierarchy window, you can see that it simply appears as a white box in the Scene view. This is because it has been defaulted to use the Orthographic mode for its Projection setting, which you can see in the Inspector panel.

    每个相机都有两个选项来呈现游戏。透视法通过利用它们与摄像机的距离来渲染一切,模仿真实世界;远离相机的对象比靠近相机的对象画得小。正交相机渲染一切都没有这个考虑;对象不会根据它们与相机的距离进行缩放。

  4. 接下来,我们需要一个场地。所以,进入 Unity 的菜单栏,导航到游戏对象 | 三维对象 | 立方体。这将作为一个简单的基础很好地工作。

  5. 要使它看起来有点像地面,创建一个绿色的材质,并将其应用于立方体游戏对象。
  6. 地面立方体需要足够大,以覆盖我们整个游戏领域。为此,将立方体的比例属性设置为 X 轴的100Y 轴的10,以及 Z 轴的5。另外,将其位置属性设置为30表示 X 轴,-5表示 Y 轴,0表示 Z 轴。因为没有东西会沿着 x 轴移动,所以地面只需要足够大就可以让我们场景中的其他物体着陆。然而,它确实需要足够宽和高,以防止相机看到边缘。
  7. 为了优化我们在 2D 游戏中使用的地面立方体,我们需要改变它的碰撞器。在层级窗口中选择立方体游戏对象,在检查器面板中查看。右键单击箱式对撞机组件,选择移除组件。接下来,在 Unity 的顶部,导航到组件 | 物理 2D | 箱式对撞机 2D 。这个组件的工作原理就像普通的箱式对撞机组件一样,只是深度没有限制。
  8. 现在,由于光线不足,地面看起来相当暗。从 Unity 的菜单栏中,导航到游戏对象 | 灯光 | 方向灯,以便为场景增加一些亮度。
  9. 接下来,我们需要防止所有将在场景中飞行的物体偏离太远并导致问题。为此,我们需要创建一些触发器卷。最简单的方法是创建三个空的游戏对象,并给每个对象一个箱式对撞机 2D 组件。确保勾选为触发复选框,以便将其更改为触发音量。
  10. 在地面物体的每一端放置一个,最后一个游戏物体在大约 50 个单位以上。然后,缩放它们,与地面形成一个盒子。每一个都不应该厚于单个单元。
  11. 为了使卷实际上防止对象偏离太远,我们需要创建一个新的脚本。创建一个新的脚本并命名为GoneTooFar
  12. 这个脚本有一个单一的,简短的功能,OnTriggerEnter2D。我们使用这个函数来销毁任何可能进入该卷的对象。Unity 的物理系统使用该功能来检测物体何时进入触发体积。稍后我们将对此进行更详细的讨论,但是现在,要知道两个对象中的一个,或者体积或者进入其中的对象,需要一个刚体组件。在我们的例子中,当它们进入触发器时,我们可能想要移除的所有东西都将有一个刚体组件:

    java public void OnTriggerEnter2D(Collider2D other) { Destroy(other.gameObject); }

  13. Finally, return to Unity and add the script to the three trigger-volume objects.

    Setting up the development environment

我们已经完成了 2D 游戏的初始设置。通过将项目类型从 3D 更改为 2D ,Unity 中的默认值将更改为针对 2D 游戏创作进行优化。最直接值得注意的是,相机现在处于正投影视图中,使一切看起来都变平了。我们还为场景创建了地面和一些触发卷。这些将一起防止我们的鸟和其他任何东西偏离太远。

物理学

在 Unity 中,物理模拟主要关注 刚体组件的使用。当刚体组件附着在任何物体上时,都会被物理引擎接管。物体会随着重力下落,并撞上任何有碰撞器的物体。在我们的脚本中,使用OnCollision组函数和OnTrigger组函数需要将刚体组件附加到两个交互对象中的至少一个上。然而,刚体组件可以干扰我们可能导致物体进行的任何特定运动。但是刚体组件可以标记为运动学,这意味着物理引擎不会移动它,但它只会在我们的脚本移动它时移动。我们用于坦克的字符控制器部件是一个特殊的、经过修改的刚体。在这一章中,我们将大量使用刚体组件来将我们所有的鸟、积木和猪连接到物理引擎中。

积木

对于我们的第一个物理对象,我们将创建建造猪城堡的积木。我们将创造三种类型的积木:木头、玻璃和橡胶。有了这几个简单的街区,我们将能够很容易地创造出各种各样的关卡和建筑,用鸟来砸。

我们将要创建的每一个区块在很大程度上都是相似的。因此,我们将从最基本的一块木板开始,并在此基础上扩展以创建其他木板。让我们使用这些步骤来创建块:

  1. 首先,我们将制作木板。为此,我们需要另一个立方体。改名Plank_Wood
  2. 对于 X 轴和 YZ 轴,将木板的刻度值设置为0.25。它在 xy 轴上的刻度定义了玩家看到的大小。 z 轴上的刻度帮助我们确保它会被场景中的其他物理物体击中。
  3. 接下来,使用plank_wood纹理创建一个新材质,并将其应用于立方体。
  4. 为了让这块新木板成为适合我们游戏的物理物体,我们需要移除立方体的箱式对撞机组件,并将其替换为箱式对撞机 2D 组件。另外,添加一个刚体组件。确保选择了您的木板;前往 Unity 的菜单栏,导航至组件 | 物理 2D | 刚体 2D
  5. 接下来,我们需要让木板在我们的游戏中正常工作;我们需要创建一个新的脚本并将其命名为Plank
  6. 这个脚本从一堆变量开始。前两个变量用于跟踪木板的健康状况。我们需要将健康的总量与当前健康分开,这样我们就能够检测到对象何时被降低到半健康状态。在这一点上,我们将利用我们接下来的三个变量来改变物体的材质,以显示损坏。最后一个变量在对象失去健康并被销毁时使用。我们会用它来增加玩家的分数:

    ```java public float totalHealth = 100f; private float health = 100f;

    public Material damageMaterial; public Renderer plankRenderer; private bool didSwap = false;

    public int scoreValue = 100; ```

  7. 对于脚本的第一个函数,我们使用Awake进行初始化。我们确保对象的当前健康状况与其总健康状况相同,并且didSwap标志设置为false :

    java public void Awake() { health = totalHealth; didSwap = false; }

  8. 接下来,我们使用OnCollisionEnter2D功能,这只是 3D 中使用的普通OnCollisionEnter功能的 2D 优化版本。这是一个特殊的功能,由刚体组件触发,它给我们关于物体碰撞了什么以及如何碰撞的信息。我们利用这些信息找到collision.relativeVelocity.magnitude。这是物体碰撞的速度,我们用这个作为伤害来降低当前的生命值。接下来,该功能检查健康状况是否降低到一半,如果降低,则调用SwapToDamaged功能。通过使用didSwap标志,我们确保该函数只被调用一次。最后,功能检查健康状况是否降至零以下。如果有,该对象被销毁,我们将很快制作的LevelTracker脚本添加到玩家的分数中:

    ```java public void OnCollisionEnter2D(Collision2D collision) { health -= collision.relativeVelocity.magnitude;

    if(!didSwap && health < totalHealth / 2f) { SwapToDamaged(); }

    if(health <= 0) { Destroy(gameObject); LevelTracker.AddScore(scoreValue); } } ```

  9. 最后,对于这个脚本,我们有SwapToDamaged功能。首先将didSwap标志设置为true。接下来,它检查以确保plankRendererdamageMaterial变量引用了其他对象。最终,它使用plankRenderer.sharedMaterial值将材质更改为看起来损坏的材质:

    ```java public void SwapToDamaged() { didSwap = true; if(plankRenderer == null) return;

    if(damageMaterial != null) { plankRenderer.sharedMaterial = damageMaterial; } } ```

  10. 在我们将Plank脚本添加到对象之前,我们需要创建前面提到的LevelTracker脚本。现在就创建它。

  11. 这个脚本相当短,从一个变量开始。该变量将跟踪玩家在该级别的得分,并且是静态的,因此它可以在对象被破坏时轻松更改,例如积分:

    java private static int score = 0;

  12. 接下来,我们使用Awake功能来确保玩家在开始一个关卡时从零开始:

    java public void Awake() { score = 0; }

  13. 最后,对于脚本,我们添加AddScore函数。这个函数只是简单地获取传递给它的点数,并增加玩家的分数。它也是静态的,所以它可以被场景中的任何对象调用,而不需要引用脚本:

    java public static void AddScore(int amount) { score += amount; }

  14. 回到 Unity,我们需要使用plank_wood_damaged纹理创建一个新的材质。这将是脚本将交换到的材质。

  15. 我们需要将Plank脚本添加到我们的Plank_Wood对象中。将损坏材质参照连接到新材质,将木板渲染器参照连接到对象的网格渲染器组件。
  16. 当我们创建不同类型的木板时,我们可以调整总健康的值,以赋予它们不同的优势。对于木板来说,值25相当有效。
  17. 接下来,创建一个空的游戏对象,并将其重命名为LevelTracker
  18. LevelTracker脚本添加到对象中,它将开始跟踪玩家的分数。
  19. 如果你想看木板的动作,把它放在地面上,然后按播放键。游戏一开始,Unity 的物理就会接管,带着重力掉落木板。如果它开始足够高,你将能够看到它切换纹理,因为它失去了健康。
  20. 要制作我们需要的另外两块木板,选择Plank_Wood对象,按 Ctrl + D 两次复制。将一块木板改名为Plank_Glass,另一块改名为Plank_Rubber
  21. 接下来,创建三种新材质。橡胶板要用紫色,玻璃板要用plank_glass纹理,玻璃板损坏时最后一种材质要用plank_glass_damaged纹理。将新材质涂在新木板的适当位置。
  22. 至于新木板的健康状况,玻璃的15值和橡胶的100值会很好。
  23. Finally, turn your three planks into prefabs and use them to build a structure for you to knock down. Feel free to scale them in order to make differently sized blocks, but leave the z axis alone. Also, all of the blocks should be positioned at 0 on the z axis and your structure should be centered around about 30 on the x axis.

    Building blocks

我们已经为将要在我们的游戏中被推倒的建筑建造了我们需要的积木。我们使用了一个刚体组件将它们连接到物理引擎中。此外,我们创建了一个脚本来跟踪他们的健康状况,并在损坏的材质降到一半以下时将其替换。对于这个游戏,我们坚持所有物理组件的 2D 优化版本。它们的工作方式与 3D 版本完全相同,只是没有第三个轴。

木材和玻璃作为基本材质工作良好。然而,如果我们要提高难度,我们需要一些更强的东西。试着做一块石头。为它创建两种纹理和材质,以显示其原始状态和损坏状态。

物理材质

物理材质是特殊类型的材质,专门告诉物理引擎两个物体应该如何相互作用。这不会影响对象的外观。它定义了对撞机的摩擦力和弹力。我们将使用它们来给我们的橡胶板一些弹性,给玻璃板一些滑动。有了这几个步骤,我们就可以快速实现物理材质创造出赏心悦目的效果:

  1. 项目面板中,物理材质以与其他所有材质相同的方式创建。右键单击项目面板,导航至创建 | 物理 2D 材质。创建两个物理材质,并命名其中一个Glass和另一个Rubber
  2. 选择其中一个,在检查器窗口中查看。2D 版本只有两个值(3D 版本有一些额外的值,但它们仅用于更复杂的情况):
    • 摩擦力:该属性控制沿表面滑动时损失的移动量。零值表示没有摩擦,如冰,值为 1 表示有很大的摩擦,如橡胶。
    • 弹力:这个属性是物体撞击到什么东西或者被什么东西撞击的时候,反射的能量有多少。零表示没有能量被反射,而值为 1 表示物体将反射所有能量。
  3. 对于Glass材质,将摩擦值设置为0.1弹性设置为0。对于Rubber材质,将摩擦力设置为1弹力设置为0.8
  4. 接下来,选择你的Plank_Glass预制体,看看它的箱式对撞机 2D 组件。要应用新的物理材质,只需将它们从项目面板一个一个拖放到材质槽中。对你的Plank_Rubber预制体也这样做,任何时候物体击中其中一个,材质将被用来控制它们的相互作用。

我们创造了一对物理材质。当两个对撞机相撞时,它们控制着如何相互作用。利用这些,我们可以控制任何碰撞器所具有的摩擦力和弹力。

字符

拥有一堆通用积木只是这个游戏的开始。接下来,我们将创建几个角色来为游戏增添一些活力。我们需要一些邪恶的猪来消灭,需要一些善良的鸟来攻击它们。

制造敌人

我们的第一个角色将是敌方猪。他们自己实际上什么都不做。所以,它们实际上只是我们之前制作的看起来像猪的木块。然而,为了让他们的毁灭成为游戏的目标,我们将扩展我们的LevelTracker脚本来观看他们,并在他们全部毁灭的情况下触发游戏结束事件。我们还将扩展脚本以更新屏幕上的分数,并使其保存分数以备后用。不像我们的木板,我们只能看到立方体的一面,猪被创建为平面纹理,并被统一的 2D 管道用作精灵。让我们从这些步骤开始,为我们的愤怒的小鸟游戏创建猪:

  1. 猪是以类似于木板的方式创造出来的;然而,他们使用一种叫做雪碧的特殊 2D 物体。精灵实际上只是一个平面物体,总是看着屏幕。大多数 2D 游戏都是用一系列精灵来制作所有的物品。您可以通过导航到游戏对象 | 2D 对象 | 精灵来创建一个。命名为Pig
  2. 要使您的新精灵看起来像头猪,请从项目面板中拖动pig_fresh图像,并将其放入精灵渲染器组件的精灵插槽中。
  3. 接下来,添加一个圆形对撞机 2D 组件和一个刚体 2D 组件。圆形对撞机 2D 组件的工作原理与我们之前使用的球体对撞机组件类似,但针对 2D 游戏进行了优化。
  4. 在在游戏中使用我们的猪之前,我们需要更新Plank脚本,这样它就可以处理精灵图像和材质的变化。所以,我们打开它,在开头添加一个变量。该变量只是跟踪要更改为哪个精灵:

    java public Sprite damageSprite;

  5. 接下来,我们需要在我们的SwapToDamaged函数的末尾添加一小部分。这个if语句检查一个精灵是否可以换成。如果是,我们将通用渲染器变量转换成SpriteRenderer,这样我们就可以访问其上的sprite变量,并更新到我们的新图像:

    java if(damageSprite != null) { SpriteRenderer spriteRend = plankRenderer as SpriteRenderer; spriteRend.sprite = damageSprite; }

  6. Plank脚本添加到猪中,并用精灵渲染器组件填充木板渲染器槽。另外,将pig_damage图像放入伤害精灵槽中。稍微改变一下这个脚本,我们以后就能省去很多麻烦,那时我们也许想追踪的不仅仅是猪的毁灭。

  7. 现在,把猪变成一个预制的,并把它添加到你的结构中。请记住,您需要将它们保持在 z 轴上的零,但可以随意调整它们的大小、健康和得分值,以给它们一些变化。
  8. 接下来,我们需要展开LevelTracker脚本。打开它,我们可以添加更多的代码。
  9. 首先,我们需要在脚本的开头添加一行,这样我们就可以编辑图形用户界面中显示的文本。就像我们之前所做的一样,在脚本的最顶部添加这一行,其中以using开头的另外两行是:

    java using UnityEngine.UI;

  10. 接下来,我们将在脚本的开头添加更多的变量。第一个,顾名思义,将包含我们场景中所有猪的列表。下一个是标志,表示游戏已经结束。我们还有三个Text变量,所以我们可以在玩家玩的时候更新他们的分数,告诉他们游戏为什么结束,以及他们的最终分数是多少。最后一个变量将允许你打开和关闭最终屏幕,在那里我们告诉玩家他们是否赢了:

    ```java public Transform[] pigs = new Transform[0];

    private gameOver = false;

    public Text scoreBox; public Text finalMessage; public Text finalScore;

    public GameObject finalGroup; ```

  11. 接下来,我们需要给Awake函数添加一行。这只是确保在游戏开始时,告知玩家游戏如何结束的一组 GUI 对象被关闭:

    java FinalGroup.SetActive(false);

  12. LateUpdate功能中,我们首先检查游戏是否已经结束。如果没有,我们调用另一个功能来检查是否所有的猪都被消灭了。我们还更新了玩家的分数显示,无论是在他们玩的时候还是在屏幕上的游戏:

    ```java public void LateUpdate() { if(!gameOver) { CheckPigs();

    scoreBox.text = "Score: " + score;
    finalScore.text = "Score: " + score;
    

    } } ```

  13. 接下来,我们添加CheckPigs功能。这个函数循环遍历猪的列表,看看它们是否都被销毁了。如果它找到一个没有被破坏的,它就退出这个函数。否则,游戏会被标记为结束,玩家会收到一条消息。我们还会关闭游戏内评分,并在一组 GUI 对象上打开游戏:

    ```java private void CheckPigs() { for(int i=0;i<pigs.Length;i++) { if(pigs[i] != null) return; }

    gameOver = true; finalMessage.text = "You destroyed the pigs!";

    scoreBox.gameObject.SetActive(false); finalGroup.SetActive(true); } ```

  14. OutOfBirds功能会被我们后面要创建的弹弓调用,当玩家用完鸟向猪发射的时候。如果游戏尚未结束,则该功能结束游戏并为玩家设置适当的消息。它还会关闭游戏内评分,并在一组 GUI 对象上打开游戏,就像之前的功能:

    ```java public void OutOfBirds() { if(gameOver) return;

    gameOver = true; finalMessage.text = "You ran out of birds!";

    scoreBox.gameObject.SetActive(false); finalGroup.SetActive(true); } ```

  15. Finally, we have the SaveScore function. Here, we use the PlayerPrefs class. It lets you easily store and retrieve small amounts of data, perfect for our current needs. We just need to provide it with a unique key to save the data under. For this, we use a short string combined with the level's index, as provided by Application.loadedLevel. Next, we use PlayerPrefs.GetInt to retrieve the last score that was saved. If there isn't one, the zero that we passed to the function is returned as a default value. We compare the new score with the old score and use PlayerPrefs.SetInt to save the new score, if it is higher. Finally, the Application.LoadLevel function can be used to load any other scene in our game. All the scenes you intend to load have to be added to the Build Settings window, found in the File menu, and can be loaded by using either their name or their index, as shown here:

    ```java public void SaveScore() { string key = "LevelScore" + Application.loadedLevel; int previousScore = PlayerPrefs.GetInt(key, 0); if(previousScore < score) { PlayerPrefs.SetInt(key, score); }

    Application.LoadLevel(0); } ```

    请注意,使用PlayerPrefs是目前为止在 Unity 中存储保存信息最简单的方法。然而,它并不是最安全的。如果你在电脑注册表中有更改数值的经验,你可以很容易地从游戏外找到并更改这些PlayerPrefs数值。这决不会使它成为存储游戏信息的坏路径。你应该意识到这一点,以防你曾经制作了一个游戏,并希望防止玩家黑客和改变他们的游戏保存值。

  16. 接下来,我们需要创建一些 GUI 对象,以便我们的玩家可以看到他们在游戏中的表现。请记住,您可以通过导航到游戏对象 | 用户界面来找到它们。我们需要三个文本对象,一个按钮和一个面板。

  17. 第一个文本对象应该命名为Score。当关卡进行时,它会显示玩家的点数。锚定并定位在画布区域的左上角。
  18. 按钮需要是面板的子级。它应该固定在屏幕的中心,并位于屏幕的正下方。还有,把按钮的文字改成有意义的东西;Return to Level Select在这里会很好用。
  19. 对于点击,我们需要点击加号添加一个新事件。选择LevelTracker脚本的SaveScore功能。否则,我们将无法记录玩家的高分并离开关卡。
  20. 最后两个文本对象也应该成为面板的子对象。说出其中一个Message;它会告诉我们的玩家为什么关卡结束了。另一个应该命名为FinalScore,当他们完成时显示玩家的分数。它们都需要固定在屏幕的中央。将FinalScore对象放置在按钮上方,并在上方显示信息。
  21. Finally, all the pig objects in our scene need to be added to the LevelTracker script's list by dragging and dropping each pig in the Pigs value under the Inspector window. Also, put each text object into its slot and the panel into the Final Group slot.

    Creating the enemy

我们创建了猪并更新了我们的LevelTracker脚本来跟踪它们。猪真的就像木板一样,但它们是圆形的,而不是盒子。更新后的LevelTracker脚本会观察所有猪被消灭的情况,并在它们被消灭时触发游戏结束画面。它还会在游戏进行过程中抽取分数,并在关卡结束时保存该分数。

我们的游戏还没有完全工作,但这并不意味着它必须看起来像 Unity 提供的默认值。运用你前几章的技巧,让这个界面元素看起来更好。即使只是改变字体,也会给我们的游戏带来巨大的改变。也许甚至可以尝试改变Panel的背景图像,在屏幕上给我们的游戏增加最后一点闪光。

创造盟友

接下来,我们需要一些东西扔向猪和它们的防御工事。在这里,我们将创造最简单的鸟类。红鸟本质上只是一块石头。除了健康之外,它没有特别的权力,它的代码也没有什么特别的地方。你还会注意到这只鸟是一个 3D 模型,给它留下了猪缺失的阴影。让我们使用这些步骤来创建红鸟:

  1. 红鸟是另一个 3D 模型,所以它的设置方式类似于木板。创建一个空的游戏对象,将其命名为Bird_Red,并从birds模型中添加适当的模型作为子模型,将其位置清零并根据需要缩放,使其大约跨越一个单位。应旋转模型,使其沿 x 轴对齐。如果向镜头多转一点,玩家就能看到小鸟的脸,同时还能给人一种俯视比赛场地的感觉。
  2. 接下来,给它一个圆形对撞机 2D 组件和一个刚体 2D 组件。
  3. 现在,我们需要创建一个名为Bird的新脚本。这个脚本将成为我们所有鸟类的基础,追踪它们的健康状况,并在适当的时候触发它们的特殊能力。
  4. 脚本从三个变量开始。第一个将记录这只鸟目前的健康状况。第二个是旗帜,所以这只鸟只会使用它的特殊力量一次。它被标记为protected,这样我们所有的鸟都可以使用它,同时保护它免受外部来源的干扰。最后一个将引用我们的刚体组件:

    java public float health = 50; protected bool didSpecial = false; public Rigidbody2D body;

  5. Update功能在激活鸟的特殊力量前做三个检查。首先,它检查是否已经完成,然后检查屏幕是否被触摸。通过检查鼠标左键,我们可以很容易地检查在这一帧中是否进行了任何数量的触摸,如果我们触摸屏幕,Unity 会触发。最后,它检查这只鸟是否有一个刚体组件,以及它是否被另一个脚本控制:

    ```java public void Update() { if(didSpecial) return; if(!Input.GetMouseButtonDown(0)) return; if(body == null || body.isKinematic) return;

    DoSpecial(); } ```

  6. 在红鸟的情况下,DoSpecial功能仅将其旗帜设置为true。它被标记为virtual,这样我们就可以覆盖其他鸟的功能,让它们做一些奇特的事情:

    java protected virtual void DoSpecial() { didSpecial = true; }

  7. OnCollisionEnter2D功能的工作方式与木板相似,根据碰撞的强度减去生命值,如果鸟失去生命值,则将其消灭:

    java public void OnCollisionEnter2D(Collision2D collision) { health -= collision.relativeVelocity.magnitude; if(health < 0) Destroy(gameObject); }

  8. 返回 Unity,将脚本添加到Bird_Red对象。

  9. 完成鸟的创作,把它变成一个预设,并把它从场景中删除。我们下一步将创建的弹弓将在游戏开始时处理鸟的创建。

我们创造了红鸟。它的设置就像我们的其他物理对象一样。我们还创建了一个脚本来处理鸟的健康。这个脚本将在我们为我们的游戏创建其他鸟类时展开。

控制

接下来,我们将给予玩家与游戏互动的能力。首先,我们将创建一个弹弓来投掷鸟。接下来,我们将创建摄像机控件。我们甚至会创建一个很好的背景效果来完善我们游戏的外观。

用弹弓攻击

要攻击猪堡垒,我们有我们基本的鸟弹药。我们需要制造一个弹弓把弹药扔向猪。它还将在关卡开始时处理鸟类的产卵,并在使用鸟类时自动重装。弹弓鸟用完时会通知LevelTracker脚本,游戏结束。最后,我们将创建一个脚本来防止物理模拟进行太久。我们不想强迫玩家坐着看一头猪慢慢滚过屏幕。因此,过一会儿,脚本将开始阻尼刚体组件的运动,使它们停止而不是继续滚动。为了做到这一切,我们将遵循以下步骤:

  1. 要开始创建弹弓,请将弹弓模型添加到场景中,并将其放置在原点。根据需要进行缩放,使其大约有四个单位高。在Fork模型上涂抹浅棕色材质,在Pouch模型上涂抹深棕色材质。
  2. Next, we need four empty GameObjects. Make them all the children of the Slingshot object.

    命名第一个游戏对象FocalPoint,并将其放在弹弓的分叉叉之间。这将是我们发射所有鸟的点。

    第二个游戏对象是Pouch。首先,将 X 轴的旋转设置为0,将90设置为 Y 轴,将0设置为 Z 轴,使蓝色箭头沿着我们的游戏区域指向前方。接下来,使pouch模型成为该对象的子对象,将其在 XY 轴上的位置设置为0,在 Z 轴上的位置设置为-0.5,其旋转为270代表 X90代表 Y ,而0代表 Z 。这将使小袋出现在当前的鸟类面前,而不必制作一个完整的小袋模型。

    第三个 GameObject 是BirdPoint;这将定位被发射的鸟。使其成为Pouch点的子点,并将其位置设置为 X 轴上的0.3,以及 YZ 轴上的0

    最后一个游戏对象是WaitPoint;等待发射的鸟将被放置在这个点的后面。将 X 轴的位置设置为-4,将 Y 轴的位置设置为0.5,将 Z 轴的位置设置为0

  3. 接下来,旋转的Fork模型,这样我们可以看到叉子的两个叉头,同时它看起来指向前方。 X 轴的270Y 轴的290Z 轴的0值将正常工作。

  4. Slingshot脚本将为玩家提供大部分的交互。现在就创建它。
  5. 我们从一组变量开始这个脚本。第一组将保留对前面提到的阻尼器的引用。第二组将跟踪将在关卡中使用的鸟。接下来是一组变量,将跟踪当前准备发射的鸟。第四,我们有一些变量来保存我们刚才创建的点的引用。maxRange变量是从焦点到玩家可以拖动眼袋的距离。最后两个变量定义了鸟的发射力度:

    ```java public RigidbodyDamper rigidbodyDamper;

    public GameObject[] levelBirds = new GameObject[0]; private Rigidbody2D[] currentBirds; private int nextIndex = 0; public Transform waitPoint; public Rigidbody2D toFireBird; public bool didFire = false; public bool isAiming = false;

    public Transform pouch; public Transform focalPoint; public Transform pouchBirdPoint;

    public float maxRange = 3;

    public float maxFireStrength = 25; public float minFireStrength = 5; ```

  6. 和其他脚本一样,我们使用Awake函数进行初始化。levelBirds变量将保存该级别中使用的所有鸟预制体的引用。我们从创建每个实例开始,并将其刚体存储在currentBirds变量中。每只鸟的刚体组件上的isKinematic变量被设置为true,这样当它不使用时就不会移动。接下来,它准备好要发射的第一只鸟,最后,它将剩余的鸟定位在waitPoint :

    ```java public void Awake() { currentBirds = new Rigidbody2D[levelBirds.Length]; for(int i=0;i<levelBirds.Length;i++) { GameObject nextBird = Instantiate(levelBirds[i]) as GameObject; currentBirds[i] = nextBird.GetComponent(); currentBirds[i].isKinematic = true; }

    ReadyNextBird(); SetWaitPositions(); } ```

    后面 7. ReadyNextBird功能首先检查我们是否用完了鸟。如果是,它会找到LevelTracker脚本,告诉它已经没有鸟可以放了。nextIndex变量跟踪玩家将要发射的鸟在列表中的当前位置。接下来,该函数将下一只鸟存储在toFireBird变量中,并使其成为我们创建的BirdPoint对象的子对象;它的位置和旋转归零。最后,重置开火和瞄准标志:

    ```java public void ReadyNextBird() { if(currentBirds.Length <= nextIndex) { LevelTracker tracker = FindObjectOfType(typeof(LevelTracker)) as LevelTracker; tracker.OutOfBirds(); return; }

    toFireBird = currentBirds[nextIndex]; nextIndex++ ;

    toFireBird.transform.parent = pouchBirdPoint; toFireBird.transform.localPosition = Vector3.zero; toFireBird.transform.localRotation = Quaternion.identity;

    didFire = false; isAiming = false; } ```

  7. SetWaitPositions功能使用waitPoint的位置来定位弹弓后面所有剩余的鸟:

    java public void SetWaitPositions() { for(int i=nextIndex;i<currentBirds.Length;i++) { if(currentBirds[i] == null) continue; Vector3 offset = Vector3.right * (i – nextIndex) * 2; currentBirds[i].transform.position = waitPoint.position – offset; } }

  8. Update功能从检查玩家是否发射了一只鸟开始,观察rigidbodyDamper.allSleeping变量,看是否所有的物理物体都停止了运动。一旦他们这么做了,下一只鸟就准备好被解雇了。如果我们没有开火,瞄准标志被检查并且DoAiming功能被调用来处理瞄准。如果玩家既没有瞄准也没有刚刚发射了一只鸟,我们检查触摸输入。如果玩家触摸的距离焦点足够近,我们标记玩家已经开始瞄准:

    java public void Update() { if(didFire) { if(rigidbodyDamper.allSleeping) { ReadyNextBird(); SetWaitPositions(); } return; } else if(isAiming) { DoAiming(); } else { if(Input.touchCount <= 0) return; Vector3 touchPoint = GetTouchPoint(); isAiming = Vector3.Distance(touchPoint, focalPoint.position) < maxRange / 2f; } }

  9. DoAiming功能检查玩家是否已经停止触摸屏幕,并在他们停止触摸屏幕时发射当前的鸟。如果没有,我们将袋子放在当前触摸点。最后,小袋的位置被限制为保持在最大范围内:

    ```java private void DoAiming() { if(Input.touchCount <= 0) { FireBird(); return; }

    Vector3 touchPoint = GetTouchPoint();

    pouch.position = touchPoint; pouch.LookAt(focalPoint);

    float distance = Vector3.Distance(focalPoint.position, pouch.position); if(distance > maxRange) { pouch.position = focalPoint.position – (pouch.forward * maxRange); } } ```

  10. GetTouchPoint功能使用ScreenPointToRay找出玩家在 3D 空间中触摸的位置。这类似于我们触摸香蕉的时候;然而,因为这个游戏是 2D,我们可以只看光线的来源,并为其 z 轴值返回一个零:

    java private Vector3 GetTouchPoint() { Ray touchRay = Camera.main.ScreenPointToRay(Input.GetTouch(0).position); Vector3 touchPoint = touchRay.origin; touchPoint.z = 0; return touchPoint; }

  11. 最后,对于这个脚本,我们有FireBird功能。该功能首先将我们的didFire标志设置为true。接下来,它通过寻找从袋的位置到focalPoint的方向来找到鸟需要被发射的方向。它还利用它们之间的距离来确定鸟需要发射的功率,将它夹在我们的最小和最大强度之间。然后,在找到它的刚体组件后,它通过清除它的母体并将其isKinematic标志设置为false来释放鸟。要启动它,我们使用AddForce功能,通过方向乘以功率。ForceMode2D.Impulse也被传递以确保所施加的力发生一次并且是立即的。接下来,袋子被定位在focalPoint处,好像它实际上处于张力之下。最后,我们调用rigidbodyDamper.ReadyDamp开始刚体组件运动的阻尼:

    ```java private void FireBird() { didFire = true;

    Vector3 direction = (focalPoint.position – pouch.position).normalized; float distance = Vector3.Distance(focalPoint.position, pouch.position); float power = distance <= 0 ? 0 : distance / maxRange; power *= maxFireStrength; power = Mathf.Clamp(power, minFireStrength, maxFireStrength);

    toFireBird.transform.parent = null; toFireBird.isKinematic = false; toFireBird.AddForce(new Vector2(direction.x, direction.y) * power, ForceMode2D.Impulse);

    pouch.position = focalPoint.position;

    rigidbodyDamper.ReadyDamp(); } ```

  12. 在我们能够利用Slingshot脚本之前,我们需要创建RigidbodyDamper脚本。

  13. 这个脚本从以下六个变量开始。前两个定义了你需要等待多长时间才能阻尼运动,以及你需要阻尼多少。接下来的两个跟踪是否可以应用阻尼以及何时开始。下一个是一个变量,它将填充一个当前场景中所有刚体的列表。最后,它有allSleeping标志,当运动停止时将被设置为true:

    ```java public float dampWaitLength = 10f; public float dampAmount = 0.9f; private float dampTime = -1f; private bool canDamp = false; private Rigidbody2D[] rigidbodies = new Rigidbody2D[0];

    public bool allSleeping = false; ```

  14. ReadyDamp功能从使用FindObjectsOfType用所有刚体填充列表开始。当您需要开始阻尼时,设置dampTime标志作为当前时间和等待时间的总和。它标志着脚本可以进行阻尼并重置allSleeping标志。最后用StartCoroutine调用CheckSleepingRigidbodies函数。这是一种特殊的调用函数的方式,使它们在后台运行,而不会阻止游戏的其他部分运行:

    ```java public void ReadyDamp() { rigidbodies = FindObjectsOfType(typeof(Rigidbody2D)) as Rigidbody2D[]; dampTime = Time.time + dampWaitLength; canDamp = true; allSleeping = false;

    StartCoroutine(CheckSleepingRigidbodies()); } ```

  15. FixedUpdate功能中,我们首先检查我们是否可以阻尼运动,是否是时候做了。如果是,我们循环通过所有刚体,对每个刚体的旋转和线速度施加阻尼。那些动态的、由脚本控制的、已经在睡觉的——意味着它们已经停止移动——被跳过:

    ```java public void FixedUpdate() { if(!canDamp || dampTime > Time.time) return;

    foreach(Rigidbody2D next in rigidbodies) { if(next != null && !next.isKinematic && !next.isSleeping()) { next.angularVelocity = dampAmount; next.velocity = dampAmount; } } } ```

  16. CheckSleepingRigidbodies功能比较特殊,会在后台运行。这可以通过函数开头的IEnumerator标志和中间的yield return null线来实现。总的来说,这些允许该功能定期暂停,并防止游戏的其余部分在等待该功能完成时冻结。该函数首先创建一个检查标志,并使用它来检查是否所有刚体都停止了移动。如果发现其中一个仍在移动,则标志设置为false,该功能暂停,直到下一帧,然后再次尝试。当它到达终点时,因为所有刚体都在睡觉,所以它将allSleeping旗设置为true,这样弹弓就可以为下一只鸟做好准备了。当玩家准备发射下一只鸟时,它也停止阻尼:

    ```java private IEnumerator CheckSleepingRigidbodies() { bool sleepCheck = false;

    while(!sleepCheck) { sleepCheck = true;

    foreach(Rigidbody2D next in rigidbodies) {
      if(next != null && !next.isKinematic && !next.IsSleeping()) {
        sleepCheck = false;
        yield return null;
        break;
      }
    }
    

    }

    allSleeping = true; canDamp = false; } ```

  17. 最后,我们有AddBodiesToCheck功能。玩家击鸟后,任何产生新物理物体的东西都可以使用这个功能。它从创建一个临时列表并展开当前列表开始。接下来,它将临时列表中的所有值添加到扩展列表中。最后,在临时列表后增加刚体列表:

    ```java public void AddBodiesToCheck(Rigidbody2D[] toAdd) { Rigidbody2D[] temp = rigidbodies; rigidbodies = new Rigidbody2D[temp.Length + toAdd.Length];

    for(int i=0;i<temp.Length;i++) { rigidbodies[i] = temp[i]; } for(int i=0;i<toAdd.Length;i++) { rigidbodies[i + temp.Length] = toAdd[i]; } } ```

  18. 返回 Unity,将两个脚本添加到Slingshot对象。在Slingshot脚本组件中,将引用连接到Rigidbody Damper脚本组件和每个点。此外,在等级鸟列表中添加尽可能多的参考红鸟预设。

  19. 为了防止物体翻滚并穿过弹弓,在Slingshot中添加一个箱式对撞机 2D 组件,并将其放置在Fork模型的库存中。
  20. 为了完成弹弓的外观,我们需要创建松紧带,将袋子绑在叉子上。我们将通过首先创建SlingshotBand脚本来做到这一点。
  21. 脚本以两个变量开始,一个是乐队将要结束的点,一个是引用将绘制它的LineRenderer变量:

    java public Transform endPoint; public LineRenderer lineRenderer;

  22. Awake功能确保lineRenderer变量只有两点,并设置它们的初始位置:

    ```java public void Awake() { if(lineRenderer == null) return; if(endPoint == null) return;

    lineRenderer.SetVertexCount(2); lineRenderer.SetPosition(0, transform.position); lineRenderer.SetPosition(1, endPoint.position); } ```

  23. LateUpdate功能中,我们将lineRenderer变量的结束位置设置为endPoint值。这个点会随着眼袋移动,所以我们需要不断更新渲染器:

    ```java public void LateUpdate() { if(endPoint == null) return; if(lineRenderer == null) return;

    lineRenderer.SetPosition(1, endPoint.position); } ```

  24. 返回统一并创建一个空的游戏对象。将其命名为Band_Near,并使其成为Slingshot对象的子对象。

  25. 作为这个新点的子点,创建一个圆柱体和第二个空的游戏对象,命名为Band
  26. 给圆柱体一个棕色的材质,把它放在弹弓叉的近端。一定要拆下胶囊对撞机组件,这样才不会碍事。此外,不要害怕缩放它,以使它更适合弹弓的外观。
  27. 组件菜单中的效果下添加线条渲染器组件到Band对象。将其定位在圆柱体中心后,将SlingshotBand脚本添加到对象中。
  28. 材质下的线条渲染器组件,你可以把你的棕色材质放在槽里给乐队上色。在参数下,将起始宽度设置为0.5,将结束宽度设置为0.2,以设置线的尺寸。
  29. 接下来,创建另一个空的游戏对象并将其命名为BandEnd_Near。使其成为Pouch对象的子对象,并将其放置在小袋内。
  30. 现在,将脚本的引用连接到它的线渲染器和端点。
  31. 要制作第二个带子,复制我们刚刚创建的四个对象,并根据叉子的另一个叉头放置它们。这条带子的端点可以沿着 z 轴向后移动,以避免鸟的干扰。
  32. Finally, turn the whole thing into a prefab so that it can be easily reused in other levels.

    Attacking with a slingshot

我们创造了一个弹弓,可以用来射鸟。我们使用我们在前一章中学习的技术来处理触摸输入,并在玩家瞄准和射击时跟踪他们的手指。如果你保存你的场景,并定位相机来看弹弓,你会注意到它是完整的,如果不是完全可玩的话。虽然我们只能从 Unity 的场景视图中看到破坏,但是可以向猪要塞发射鸟。

用相机观看

在这一点上,游戏在技术上是可以玩的,但是很难看出发生了什么。接下来,我们将创建一个系统来控制摄像机。该系统将允许玩家左右拖动相机,当鸟被发射时跟随,当一切停止移动时返回弹弓。还会有一组限制,以防止相机走得太远,观看我们不想让玩家看到的东西,例如超出我们为关卡创建的地面或天空的边缘。我们只需要一个相当短的脚本来控制和管理我们的相机。让我们按照以下步骤创建它:

  1. 为了开始并保持一切井然有序,创建一个新的空的游戏对象并命名为CameraRig。此外,为了简单起见,将其在每个轴上的位置设置为零。
  2. 接下来,再创建三个空的游戏对象,并将它们命名为LeftPointRightPointTopPoint。将它们的 Z 轴位置设置为-5。将LeftPoint物体放在弹弓和 Y 轴上的3前面。RightPoint对象需要位于您创建的猪结构的前面。TopPoint物体可以在弹弓上方,但需要在 Y 轴上设置为8。这三点将定义我们的相机在被拖动和跟随鸟类时可以移动的极限。
  3. 使所有三个点和Main Camera对象成为CameraRig对象的子对象。
  4. 现在,我们创建CameraControl脚本。这个脚本将控制所有的移动和与相机的交互。
  5. 我们这个脚本的变量从弹弓的引用开始;我们需要这个,这样我们就可以在当前的鸟被发射时跟踪它。接下来是对我们刚刚创建的点的引用。下一组变量控制相机在没有输入的情况下会停留多长时间,然后返回来看弹弓,以及它会以多快的速度返回。dragScale变量控制当玩家用手指在屏幕上拖动时,摄像机实际移动的速度,允许你用手指保持场景移动。最后一组控制摄像机是否能跟踪当前的鸟以及它能以多快的速度这样做:

    ```java public Slingshot slingshot; public Transform rightPoint; public Transform leftPoint; public Transform topPoint;

    public float waitTime = 3f; private float headBackTime = -1f; private Vector3 waitPosition; private float headBackDuration = 3f;

    public float dragScale = 0.075f;

    private bool followBird = false; private Vector3 followVelocity = Vector3.zero; public float followSmoothTime = 0.1f; ```

  6. Awake功能中,我们首先确定相机没有跟踪一只鸟,并让它在前往之前等待,看一看弹弓。这可以让你在关卡开始时将相机指向猪堡垒,并在给玩家一个机会看到他们面对的是什么后移动到弹弓上:

    java public void Awake() { followBird = false; StartWait(); }

  7. StartWait功能设置它开始返回弹弓的时间,并记录它返回的位置。这允许您创建平滑过渡:

    java public void StartWait() { headBackTime = Time.time + waitTime; waitPosition = transform.position; }

  8. 接下来,我们有Update功能。这个功能从检查弹弓是否已经发射开始。如果没有,它会检查玩家是否已经开始瞄准,发出信号表示应该跟踪这只鸟,如果已经瞄准,则将速度归零。如果他们没有开始瞄准,followBird标志被清除。接下来,这个函数检查它是否应该跟随,如果应该的话,也调用StartWait函数——以防这是鸟被消灭的框架。如果它不应该跟随鸟,它会检查触摸输入,如果找到,它会拖动相机。等待再次开始,以防玩家将手指移出此帧。最后,它检查弹弓是否已经发射完当前的鸟,是否是时候返回了。如果两者都是真的,摄像机移回指向弹弓:

    ```java public void Update() { if(!slingshot.didFire) { if(slingshot.isAiming) { followBird = true; followVelocity = Vector3.zero; } else { followBird = false; } }

    if(followBird) { FollowBird(); StartWait(); } else if(Input.touchCount > 0) { DragCamera(); StartWait(); }

    if(!slingshot.didFire && headBackTime < Time.time) { BackToLeft(); } } ```

  9. FollowBird功能通过检查Slingshot脚本上的toFireBird变量,确定有一只鸟跟着来启动,如果没有找到鸟,则停止跟随。如果有一只鸟,该函数然后确定一个新的移动点,它将直接看鸟。然后它使用Vector3.SmoothDamp功能平滑地跟随鸟。这个函数的工作原理类似于弹簧——它离目标位置越远,移动物体的速度就越快。followVelocity变量用于保持其平稳运行。最后,它调用另一个函数来限制摄像机在我们之前设置的边界点内的位置:

    ```java private void FollowBird() { if(slingshot.toFireBird == null) { followBird = false; return; }

    Vector3 targetPoint = slingshot.toFireBird.transform.position; targetPoint.z = transform.position.z;

    transform.position = Vector3.SmoothDamp(transform.position, targetPoint, ref followVelocity, followSmoothTime); ClampPosition(); } ```

  10. DragCamera功能中,我们使用当前触摸的deltaPosition值来确定它自上一帧以来移动了多远。通过缩放该值并从摄像机位置减去矢量,该功能可以在玩家拖动屏幕时移动摄像机。该功能还调用ClampPosition功能来保持摄像机在游戏区域内的位置:

    java private void DragCamera() { transform.position -= new Vector3(Input.GetTouch(0).deltaPosition.x, Input.GetTouch(0).deltaPosition.y, 0) * dragScale; ClampPosition(); }

  11. ClampPosition功能从拍摄相机当前位置开始。然后,它将x位置夹在leftPointrightPoint变量的x位置之间。接下来,y位置被夹在leftPointtopPoint变量的y位置之间。最后,新位置被重新应用到相机的变换:

    java private void ClampPosition() { Vector3 clamped = transform.position; clamped.x = Mathf.Clamp(clamped.x, leftPoint.position.x, rightPoint.position.x); clamped.y = Mathf.Clamp(clamped.y, leftPoint.position.y, topPoint.position.y); transform.position = clamped; }

  12. 最后,我们有BackToLeft功能。它首先使用时间和我们的持续时间变量来确定相机返回弹弓的进度。它记录摄像机的当前位置,并使用 xy 轴上的Mathf.SmoothStep找到位于waitPosition变量和leftPoint变量之间适当距离的新位置。最后,申请新职位:

    java private void BackToLeft() { float progress = (Time.time – headBackTime) / headBackDuration; Vector3 newPosition = transform.position; newPosition.x = Mathf.SmoothStep(waitPosition.x, leftPoint.position.x, progress); newPosition.y = Mathf.SmoothStep(waitPosition.y, leftPoint.position.y, progress); transform.position = newPosition; }

  13. 接下来,返回 Unity,将新脚本添加到Main Camera对象。连接弹弓和每个点的参考,完成它。

  14. 把摄像机对准你的猪堡垒,把整个装备变成一个预制的。

我们创建了一个摄像机装置,让玩家在玩游戏时观看所有的动作。摄像机现在将跟随鸟从弹弓发射,现在可以被玩家拖动。通过键入几个物体的位置,这个动作被限制,以防止玩家看到我们不想让他们看到的东西;如果相机闲置足够长的时间,它也会回来看弹弓。

相机的另一个功能是缩放手势,这在许多手机游戏中都很常见。对于用户来说,这是一个如此简单的手势,但是对于我们来说,很好地实现它可能是复杂的。试着在这里实现它。可以使用Input.touchCount检测是否有两个手指在触摸屏幕。然后,使用Vector2.Distance功能,如果您已经记录了与最后一帧的距离,就可以确定它们是相向移动还是远离移动。一旦你确定了你的变焦方向,只需改变相机的ortographicSize变量来改变能看到多少;一定要包括一些限制,这样玩家就不能永远放大或缩小。

现在我们已经有了制作一个完整关卡所需的所有部件,我们还需要一些关卡。我们至少还需要两层。你可以用积木和小猪创造任何你想要的关卡。这是一个好主意,让建筑集中在与我们的第一级相同的地方,让玩家更容易处理它们。此外,在制作关卡时考虑关卡的难度,这样你就可以得到一个简单、中等和困难的关卡。

创建视差背景

许多 2D 游戏的一大特色是视差滚动背景。这仅仅意味着背景是在以不同速度滚动的层中创建的。把它想象成你正看着窗外。远处的物体似乎很难移动,而近处的物体移动很快。在 2D 的游戏中,它给人一种深度的错觉,并给游戏的外观增加了一种美好的感觉。在这个背景下,我们将在一个平面上层叠几种材质。还有其他几种方法来创建同样的效果,但是我们的方法将使用一个单独的脚本来控制每一层的速度。让我们按照以下步骤创建它:

  1. 我们将从创建ParallaxScroll脚本开始这一部分。
  2. 这个脚本从三个变量开始。前两个变量跟踪每种材质及其滚动速度。第三个跟踪摄像机的最后位置,所以我们可以跟踪它在每一帧中移动了多远:

    ```java public Material[] materials = new Material[0]; public float[] speeds = new float[0];

    private Vector3 lastPosition = Vector3.zero; ```

  3. Start功能中,我们记录摄像机的起始位置。我们这里用Start代替Awake,以防游戏开始摄像头需要做什么特殊动作:

    java public void Start() { lastPosition = Camera.main.transform.position; }

  4. 接下来,我们使用LateUpdate功能在摄像机移动后进行更改。它首先找到摄像机的新位置,并比较 x 轴的值,以确定它移动了多远。接下来,它遍历材质列表。循环首先使用mainTextureOffset收集其纹理的当前偏移。接下来,从偏移的 x 轴减去相机移动乘以材质速度,以找到新的水平位置。然后,新的偏移将应用于材质。最后,该功能记录下一帧相机的最后位置:

    ```java public void LateUpdate() { Vector3 newPosition = Camera.main.transform.position; float move = newPosition.x – lastPosition.x;

    for(int i=0;i<materials.Length;i++) { Vector2 offset = materials[i].mainTextureOffset; offset.x -= move * speeds[i]; materials[i].mainTextureOffset = offset; }

    lastPosition = newPosition; } ```

  5. 返回 Unity,创造六种新材质。每个背景纹理一个:skyhills_tallhills_shortgrass_lightgrass_darkfronds。除sky外,所有材质都需要使用透明 渲染模式。如果没有,我们将无法看到分层时的所有纹理。

  6. 在我们可以平铺背景中的图像之前,我们需要调整它们的导入设置。依次选择每一个,查看检查器窗口。因为我们选择了制作一个 2D 游戏,Unity 默认情况下会将所有图像导入为精灵,这样可以夹紧图像的边缘,防止重复。对于我们所有的背景图像,将纹理类型选项更改为纹理,将环绕模式选项更改为重复。这将让我们使用它们的方式,使它看起来像一个无限滚动的背景。
  7. 我们还需要为每种新材质调整平铺选项。对于所有这些,将 Y 轴保留为1。对于 X 轴,设置sky5hills_tall6hills_shot7grass_dark8fronds9grass_light10。这将抵消纹理的所有特征,因此长平移看不到规则排列的特征。
  8. 接下来,创建一个新平面。将其命名为Background并移除其网格碰撞器组件。另外,附上我们的ParallaxScroll脚本。
  9. 将其定位在 X 轴上的30、在 Y 轴上的7以及在 Z 轴上的10。将其旋转设置为 X 轴的90Y 轴的180Z 轴的0。另外,将 X 轴的刻度设置为10,将 Y 轴的刻度设置为1,将 Z 轴的刻度设置为1.5。总的来说,这些将平面定位为面向相机并填充背景。
  10. 在平面的网格渲染器组件中,展开材质列表,并将尺寸的值设置为6。按照skyhills_tallhills_shortgrass_darkfrondsgrass_light的顺序将我们的每种新材质添加到列表槽中。对视差卷轴脚本组件中的材质列表进行同样的操作。
  11. 最后,在视差滚动脚本组件中,将速度列表的大小的值设置为6,并按照0.030.0240.0180.0120.0060的顺序输入以下值。这些值将轻轻地、均匀地移动材质。
  12. At this point, turning the background into a prefab will make it easy to reuse later.

    Creating the parallax   class=

我们创建了一个视差卷轴效果。这个效果会平移一系列背景纹理,在我们的 2D 游戏中给人深度的错觉。要轻松看到它的动作,在场景视图中按下播放按钮并抓住相机,将其左右移动以查看背景变化。

我们还有另外两个层次可以添加背景。你在这里的挑战是创造你自己的背景。使用你在这一节学到的技巧来创造一个夜间风格的背景。它可以包括一个静止的月亮,而其他一切都在镜头中滚动。对于一个额外的技巧,创建一个云层,慢慢地平移整个屏幕,以及相机和背景的其余部分。

增加更多鸟类

我们需要为我们的级别创建最后一组素材:其他鸟类。我们将再创造三种各有独特特殊能力的鸟:一种加速的黄色鸟,一种分裂成多种鸟的蓝色鸟,一种爆炸的黑色鸟。有了这些,我们的羊群就完整了。

为了让这些鸟的创造更容易,我们将利用一个叫做继承的概念。继承允许脚本扩展它正在继承的功能,而不需要重写它们。如果使用正确,这可能非常强大,在我们的例子中,将有助于快速创建多个大体相似的角色。

黄色的鸟

首先,我们将创建黄色的鸟。很大程度上,这种鸟的功能与红鸟完全一样。然而,当玩家第二次触摸屏幕时,鸟的特殊能力被激活,其速度增加。通过扩展我们之前创建的Bird脚本,这只鸟的创建变得非常简单。由于继承的力量,我们在这里创建的脚本只包含几行代码。让我们按照以下步骤创建它:

  1. 首先用与红鸟相同的方法创建黄鸟,改为使用YellowBird模型。
  2. 我们将创建YellowBird脚本,而不是使用Bird脚本。
  3. 这个脚本需要扩展Bird脚本,所以在我们新脚本的第四行用Bird替换MonoBehaviour。它应该类似于下面的代码片段:

    java public class YellowBird : Bird {

  4. 这个脚本添加了一个变量,用来乘以鸟的当前速度:

    java public float multiplier = 2f;

  5. 接下来,我们覆盖DoSpecial函数,并在鸟的body.velocity变量被调用时将其相乘:

    java protected override void DoSpecial() { didSpecial = true; body.velocity *= multiplier; }

  6. 返回 Unity,将脚本添加到你的新鸟中,连接刚体组件引用,并将其变成一个预置。添加一些到你的弹弓列表中,以便在你的水平使用鸟。

我们创造了黄色的鸟。这只鸟很简单。当玩家触摸屏幕时,它直接修改其速度,以突然获得速度提升。正如你很快会看到的,我们使用相同风格的脚本来创建我们所有的鸟。

蓝鸟

接下来,我们将创建蓝鸟。当玩家触摸屏幕时,这只鸟分裂成三只鸟。它还将通过使用继承来扩展Bird脚本,减少创建鸟需要编写的代码量。让我们按照以下步骤进行:

  1. 再次,开始构建你的蓝鸟,就像前面两个鸟一样,替换适当的模型。您还应该调整圆形对撞机 2D 组件的半径的值,以与这只鸟的小尺寸适当对齐。
  2. 接下来,我们创建BlueBird脚本。
  3. 再次调整第四行,使脚本扩展Bird而不是MonoBehaviour :

    java public class BlueBird : Bird {

  4. 这个脚本有三个变量。第一个变量是当鸟分裂时产生的预置列表。接下来是将要发射的每只新鸟之间的角度差。最后一个变量是一个值,它可以让鸟在它们当前位置的前面一点产卵,以防止它们被困在对方体内:

    java public GameObject[] splitBirds = new GameObject[0]; public float launchAngle = 15f; public float spawnLead = 0.5f;

  5. 接下来,我们覆盖DoSpecial功能,并像其他功能一样,通过标记我们进行了特殊的移动来开始。接下来,它计算要产卵的鸟类数量的一半,并创建一个空列表来存储新产卵的鸟类的刚体:

    ```java protected override void DoSpecial() { didSpecial = true;

    int halfLength = splitBirds.Length / 2; Rigidbody2D[] newBodies = new Rigidbody2D[splitBirds.Length]; ```

  6. 该函数继续循环遍历鸟的列表,跳过空的槽。它在新鸟所在的位置产卵;在尝试存储对象的刚体后,如果它丢失了,它会继续下一个。新的刚体组件存储在列表中:

    ```java for(int i=0;i<splitBirds.Length;i++) { if(splitBirds[i] == null) continue;

    GameObject next = Instantiate(splitBirds[i], transform.position, transform.rotation) as GameObject;

    Rigidbody2D nextBody = next.GetComponent(); if(nextBody == null) continue;

    newBodies[i] = nextBody; ```

  7. 使用 Quaternion.Euler,一个新的旋转被创建,新的鸟将沿着从主路径分离的路径倾斜。新鸟的速度被设置为当前鸟的旋转速度。计算一个偏移量,然后沿着新路径向前移动,以避开正在繁殖的其他鸟类:

    java Quaternion rotate = Quaternion.Euler(0, 0, launchAngle * (i – halfLength)); nextBody.velocity = rotate * nextBody.velocity; Vector2 offset = nextBody.velocity.normalized * spawnLead; next.transform.position += new Vector3(offset.x, offset.y, 0); }

  8. 循环结束后,该功能使用FindObjectOfType找到当前场景中的弹弓。如果找到了,它会被改变以追踪第一只新鸟,因为它是被发射的。新的刚体列表也被设置为rigidbodyDamper变量,以便添加到其刚体列表中。最后,剧本摧毁了它所依附的鸟,完成了鸟已经分裂的假象:

    ```java Slingshot slingshot = FindObjectOfType(typeof(Slingshot)) as Slingshot; if(slingshot != null) { slingshot.toFireBird = newBodies[0]; slingshot.rigidbodyDamper.AddBodiesToCheck(newBodies); }

    Destroy(gameObject); } ```

  9. 在你把脚本添加到你的新小鸟之前,我们实际上需要两只蓝鸟:一只会分裂,一只不会分裂。复制你的鸟,命名一个Bird_Blue_Split和另一个Bird_Blue_Normal。对于分裂的鸟,添加新的脚本,对于正常的鸟,添加Bird脚本。

  10. 把这两只鸟都变成预制的,然后把正常的鸟添加到另一只鸟的列表中。

我们创造了蓝色的鸟。当用户点击屏幕时,这只鸟会分裂成多只鸟。这种效果实际上需要两只看起来完全相同的鸟,一只进行分裂,另一只被一分为二,但没有什么特别之处。

实际上,可以将我们想要产卵的任何东西添加到蓝鸟的列表中,以便拆分。你在这里的挑战是创造一只彩虹鸟。这种鸟可以分裂成不同类型的鸟,不仅仅是蓝色的。或者,也许它是一只分裂成石块的石鸟。对于一个扩展的挑战,创建一只神秘的鸟,当它分裂时,它会从列表中随机选择一只鸟。

黑鸟

最后我们有了黑鸟。当玩家触摸屏幕时,这只鸟就会爆炸。和前面讨论的所有鸟一样,它将扩展Bird脚本;对红鸟的继承让黑鸟的创作变得容易多了。让我们使用以下步骤来完成:

  1. 和其他鸟一样,这只鸟最初是以和红鸟相同的方式创建的,根据它增加的尺寸重新调整了你的圆形对撞机 2D 组件上的半径的值。
  2. 再次,我们创建一个新的脚本来扩展Bird脚本。这一次,它被称为BlackBird
  3. 别忘了调整第四行来扩展Bird脚本而不是MonoBehaviour :

    java public class BlackBird : Bird {

  4. 这个脚本有两个变量。第一个变量是爆炸的大小,第二个变量是它的强度:

    java public float radius = 2.5f; public float power = 25f;

  5. 我们再次覆盖DoSpecial功能,首先标记我们这样做了。接下来,我们使用Physics2D.OverlapCircleAll来获取在鸟的爆炸范围内的所有物体的列表,其 3D 版本是Physics.OverlapSphere。接下来,我们计算爆炸来自哪里,这只是我们的鸟的位置向下移动了三个单位。我们把它往下移是因为把碎片向上抛的爆炸比把碎片推出去的爆炸更令人兴奋。然后,该函数在列表中循环,跳过任何空槽和没有刚体的槽:

    ```java protected override void DoSpecial() { didSpecial = true;

    Collider2D[] colliders = Physics2D.OverlapCircleAll(transform.position, radius);

    Vector2 explosionPos = new Vector2(transform.position.x, transform.position.y) – (Vector2.up * 3);

    foreach(Collider2D hit in colliders) { if(hit == null) continue; if(hit.attachedRigidbody != null) { ```

  6. 如果物体存在并且附加了刚体组件,我们需要计算爆炸将如何影响该物体,模拟爆炸强度随着距离越远而减小的方式。首先,我们通过抓取另一个对象的位置来节省一些打字时间。接下来,我们计算它在哪里,相对于爆炸的位置。通过将相对位置的大小或长度除以我们的radius变量,我们可以计算出要对被击中的物体施加多大的力。最后,我们使用AddForceAtPosition踢物体,就好像爆炸发生在特定的位置。ForceMode2D.Impulse变量用于立即施加力:

    java Vector3 hitPos = hit.attachedRigidbody.transform.position; Vector2 dir = new Vector2(hitPos.x, hitPos.y) – explosionPos; float wearoff = 1 – (dir.magnitude / radius); Vector2 force = dir.normalized * power * wearoff; hit.attachedRigidbody.AddForceAtPosition(force, explosionPos, ForceMode2D.Impulse); } }

  7. 最后,该功能摧毁爆炸的鸟:

    java Destroy(gameObject); }

  8. 和前两个一样,把你的新脚本应用到你的新鸟上,并把它变成一个预置。现在,在为每个关卡选择弹弓武器库时,你有四只鸟可供选择。

我们创造了第四只也是最后一只鸟:黑鸟。当用户触摸屏幕时,这种鸟会爆炸,将任何可能靠近它的东西扔向天空。这可能是一只有趣的鸟,可以用来玩,也可以用来摧毁你的猪堡。

我们正在模仿的游戏中的黑鸟有一个额外的能力,当它击中某物后会定时爆炸。尝试为我们的黑鸟创建一个计时器来重现这种效果。您必须覆盖OnCollisionEnter功能来启动计时器,并使用LateUpdate倒计时。一旦你的计时器用完了,你可以使用我们的DoSpecial功能来真正引起爆炸。

既然你知道如何引起爆炸,我们有另一个挑战:创造一个爆炸板条箱。你需要扩展Plank脚本来制作它,当对板条箱造成足够的伤害时,触发爆炸。另一个挑战是,不要让板条箱爆炸,而是将其配置为投掷一些炸弹,当它们击中某物时会爆炸。

The black bird

等级选择

最后,我们需要创建我们的级别选择屏幕。从这个场景中,我们将能够访问并开始播放我们之前创建的所有关卡。我们还将显示每个级别的当前高分。一个新的场景和一个单独的脚本将很好地帮助我们管理我们的级别选择。让我们使用以下步骤来完成:

  1. 最后一部分从保存我们当前的场景开始,按下 Ctrl + N 创建一个新的场景;我们将其命名为LevelSelect
  2. 对于这个场景,我们需要创建一个单一的、简短的脚本,也命名为LevelSelect
  3. 这个脚本将与图形用户界面中的按钮一起工作,告诉玩家高分和负载水平。然而,在我们这样做之前,我们需要在脚本的最开始添加一行,以及其他using行——就像我们已经创建的需要更新 GUI 的其他脚本一样:

    java using UnityEngine.UI;

  4. 第一个也是唯一的变量是我们想要更新的所有按钮文本的列表,以及与它们相关联的级别的高分:

    java public Text[] buttonText = new Text[0];

  5. 第一个功能是Awake功能。在这里,它遍历所有按钮,找到最高分,并更新文本以显示它。PlayerPrefs.GetInt与我们之前用来保存高分的SetInt功能相反:

    java public void Awake() { for(int i=0;i<buttonText.Length;i++) { int levelScore = PlayerPrefs.GetInt("LevelScore" + (i + 1), 0); buttonText[i].text = "Level " + (i + 1) + "\nScore: " + levelScore; } }

  6. 这个脚本的第二个也是最后一个功能是LoadLevel。它将从图形用户界面按钮接收一个数字,并使用它来加载玩家想要玩的级别:

    java public void LoadLevel(int lvl) { Application.LoadLevel(lvl); }

  7. 返回 Unity,将脚本添加到Main Camera对象。

  8. 接下来,我们需要创建三个按钮。没有这些,我们的玩家将无法选择一个级别来玩。将每一个200单位做成正方形,并在屏幕中央排成一行。另外,将字体大小的值增加到25,这样文本就容易阅读了。
  9. 将每个按钮的Text子级拖动到Main Camera组件的级别选择脚本组件上的按钮文本列表中。他们在此列表中的排序方式是他们的文本和高分信息被更改的顺序。
  10. 另外,每个按钮都需要一个新的点击事件。选择对象的Main Camera,然后导航至级别选择功能的 | 负载级别(int) 。然后,每个按钮都需要一个数字。在按钮文本列表中有文本子级的按钮应该有编号1,因为它将显示一级信息。第二个有2,第三个有3,以此类推。每个按钮必须与列表中的顺序具有相同的编号,否则它们将导致加载不同于玩家期望的级别。
  11. Finally, open Build Settings and add your scenes to the Scenes in Build list. Clicking and dragging on the scenes in the list will let you reorder them. Make sure that your LevelSelect scene is first and has an index of zero at the right-hand side. The rest of your scenes can appear in whatever order you desire. However, beware as they will be associated with the buttons in the same order.

    Level selection

我们已经创建了一个级别选择屏幕。它有一个与我们游戏中的关卡相关的按钮列表。当按下按钮时,Application.LoadLevel开始该级别。我们还利用PlayerPrefs.GetInt检索每个级别的高分。

在这里,挑战是设计图形用户界面的风格,使屏幕看起来很棒。一个标志和一个背景会有很大帮助。此外,如果您有三个以上的级别,请查看滚动条图形用户界面对象。该对象将允许您创建一个函数,当用户滚动查看比屏幕上容易看到的大得多的级别列表时,该函数将偏移级别按钮。

总结

在这一章中,我们学习了 Unity 中的物理知识,并重现了非常受欢迎的手机游戏愤怒的小鸟。使用 Unity 的物理系统,我们能够制作出我们想要玩的所有关卡。通过这个游戏,我们还探索了 Unity 的 2D 管道,以创建伟大的 2D 游戏。我们的鸟和弹弓都是 3D 素材,让我们能够对它们进行明暗处理。然而,猪和背景是 2D 图像,减少了我们的照明选择,但允许素材的更多细节。2D 图像在创建背景的视差滚动效果方面也至关重要。最后,组成关卡的区块看起来像是 2D,但实际上是 3D 区块。我们还创建了一个级别选择屏幕。从这里,玩家可以看到他们的高分,并选择我们创建的任何级别。

下一章,我们回到上一章开始的猴球游戏。我们将创建并添加所有结束游戏的特效。我们将添加每个猴球游戏需要的弹跳和爆音效果。我们还将添加各种粒子效果。当香蕉被收集时,它们会产生一个小爆炸,而不仅仅是消失。