Unity

使用 Unity 和 C#(第 2 部分)开发您的首个游戏

Adam Tuliper

下载代码示例

欢迎回到关于 Unity 的系列教程。在第一篇文章中,我介绍了一些有关 Unity 的基础知识和体系结构。在这篇文章中,我将在 Unity 中探讨 2D,其建立于在 4.3 版本中添加的 2D 支持 Unity 的基础之上。您可以在 Unity 4.3 之前的 Unity 版本中制作 2D,但是如果没有第三方工具包,那么这个过程是相当痛苦的。我要做的就是将图片拖放到我的场景,并通过拖/放界面让它按照我希望的形式出现和发挥作用。这是 Unity 4.3 带来的一些功能,在本文中,我将在开发基本的 2D 平台游戏的同时,介绍它的更多功能和一些必要的 Unity 概念。

Unity 中的 2D

为了在 Unity 中获得 2D 支持,当在创建一个新的项目时,请在新建项目对话框的下拉列表中选择 2D。当您选择了 2D 之后,默认情况下项目会被设置为 2D(在“编辑|项目设置|编辑器”下查看),任何导入到项目中的图像都显示为 sprite 类型,而不仅仅是纹理类型。(我将在下一节对其进行介绍。)此外,场景视图默认设置为 2D 模式。这只是提供了一个帮助按钮,帮助您在场景开发过程中固定两轴,但对实际游戏没有影响。您可以在任何时候单击它,跳进跳出 2D 工作模式。Unity 中的 2D 游戏也还是一个 3D 环境;您的工作只受限于 X 轴和 Y 轴。图 1图 2 显示了选中和未选中的 2D 模式。我让照相机突出显示,以便您可以看到照相机可视区域的轮廓,但要注意,它向外的视角空间为矩形形状。

选择 2D 模式——照相机保持焦点
图 1 选择 2D 模式——照相机保持焦点

未选择 2D 模式——照相机保持焦点
图 2 未选择 2D 模式——照相机保持焦点

突出显示的照相机被设置为正交照相机,即 Unity 中的两种相机模式之一。这种照相机的类型,通常在 2D 中使用,不能在所见范围之内进一步缩放对象;即从照相机的位置看过去,没有深度。另一个照相机类型是透视图,呈现的是眼睛看到的有深度的对象。出于各种原因而使用其中一种照相机类型而不用另一种,但在一般情况下,如果需要视觉深度,可以选择透视相机,除非您想相应地放大您的对象。您只需选择照相机和改变投影类型就可以轻松更改模式。我建议您尝试上述操作,在您将物体向着 Z 轴方向移动时看看您的照相机的可视区域是如何变化的。您可以在任何时候更改默认的行为模式,这只会对未来将图像导入到您的项目产生影响。

如果在 Unity 中存在现有项目或者您不清楚是否已经在项目对话框中选择了 2D,则可以前往“编辑|项目设置|编辑器”将您的项目设置为默认 2D;否则,就必须对每个导入的 2D 图像手动设置纹理类型,如果您的作品很多,这会是一项乏味的工作。

关于 Sprite 的全部内容

当将 3D 选作默认的行为模式时,图像被识别为“纹理”类型。您不能将纹理拖到场景中;纹理必须应用到对象中。对于创建 2D 游戏来说,这种方法不是很有趣。我只想拖放图像,并让它出现在我的场景中。如果您的默认行为模式是 2D,事情就变得容易多了。当我将图像拖放到 Unity 中后,它被识别为 Sprite 类型。

这使您可以轻松将您的作品拖放到 Unity 中,然后从 Unity 将其拖放到您的场景中,从而构建游戏。如果您的作品看起来比较小,而且不需要处处重新调整,那么您只需要将像素缩小到单位值大小。这种操作方法普遍存在于 Unity 的 2D 和 3D 模式中,通常比通过 transform 的缩放属性来缩放对象具有更佳的性能。

当释放对象时,您可能会注意到对象们一个接一个地完成。Unity 在场景后面创建一系列顶点,即便 2D 图像也是如此,所以绘制顺序可以根据图像的各个部分而有所不同。通常最好的做法是明确指定图像的 Z 轴的位置。为此,您可以通过三种方法来实现,这些方法按照 Unity 绘制 sprite 的顺序列出:

  1. 在 Sprite 渲染器中设置“层排序”属性。
  2. 在 Sprite 渲染器上设置“层顺序”属性。
  3. 设置 Transform 的 Z 轴位置值。

层排序的优先级高于一切,其次是层顺序,而这反过来又优先于 transform 的 Z 值。

层排序按照定义顺序绘制。当您添加其他层时(在“编辑|项目设置|标签和图层”中),Unity 首先绘制出它在默认层上找到的所有对象(然后是层顺序,再然后是 Transform 的 Z 轴位置值),然后是背景,再然后是平台,等等。所以,您可以通过将图像设置到平台层,并指定您想在第 1 个层顺序中放在顶部的图像,从而轻松解决重叠的这些图像,因此可以在 0 层顺序后继续进行绘制。

常用功能

图 3 所示的关卡包含了一些通过拖放和设置层排序布置的平台和背景图像。

游戏关卡
图 3 游戏关卡

就现在而言,它看起来像一个游戏,却没法玩。至少,它需要一些功能才能成为一款功能性的游戏。我将在下面的章节中讨论这些内容。

键盘、鼠标和触摸移动在 Unity 中,键盘、鼠标、加速计和触摸都是通过输入系统来读取的。您可以在主播放器上使用类似于下方列出的脚本,轻松读取输入移动和鼠标点击或触摸(我将很快根据此脚本构建。):

void Update()
{
  // Returns -1 to 1
  var horizontal = Input.GetAxis("Horizontal");
  // Returns true or false. A left-click
  // or a touch event on mobile triggers this.
  var firing = Input.GetButtonDown("Fire1");
}

如果您选中“编辑|项目设置|输入”,就可以看到默认的输入(Unity 在每个新项目中为您提供一组),或设定新的输入。图 4 显示了读取水平运动的默认设置。“左”和“右”设置表示左右箭头键,但还要注意的是“a”和“d”用于水平运动。这些都可以映射到游戏杆输入。您可以添加新的输入或更改默认输入。敏感字段控制 Unity 从 0 移到 1 或 -1 的速度。当按下右箭头键时,第一帧可能产生一个 .01 的数值,然后很快达到 1,虽然可以调整速度,为您的角色指定瞬时水平速度或运动速度。稍后不久,我会向您展示将这些值应用到游戏对象的代码。不存在读取这些值所需的实际的 GameObject 组件;您只需在您的代码中使用输入关键字来访问可读取输入的功能。作为一般规则,应以 Update 函数读取输入,而不是 FixedUpdate,以避免丢失输入事件。

水平输入默认值
图 4 水平输入默认值

直线运动物体需要能够移动。如果这是一个自上而下的游戏,重力通常并不重要。如果它是一个平台游戏,重力是极为重要的。无论是哪种情况,对象的碰撞检测是至关重要的。以下是一些基本规则。添加到游戏对象的 Rigidbody2D 或 RigidBody(用于 3D)组件会自动提供该组件的质量,并使其了解重力和受力。根据维基百科,“在物理学上,刚体是一个忽略了变形的理想化实体。换句话说,刚体上任意给定两点间的距离在时间上保持不变,与外界施加的外力无关。”同样的原则也适用于游戏。添加一个刚体使您可以执行类似图 5 中所示的调用。

图 5 添加运动和速度

void FixedUpdate()
{
  // -1 to 1 value for horizontal movement
  float moveHorizontal = Input.GetAxis("Horizontal");
  // A Vector gives you simply a value x,y,z, ex  1,0,0 for
  // max right input, 0,1,0 for max up.
  // Keep the current Y value, which increases 
  // for each interval because of gravity.
  var movement = new Vector3(moveHorizontal * 
    _moveSpeed, rigidbody.velocity.y, 0);
  rigidbody.velocity = movement;
  if (Input.GetButtonDown("Fire1"))
  {
    rigidbody.AddForce(0, _jumpForce, 0);
  }
}

作为一般规则,直线运动应通过 Update 发生,加速运动应通过 FixedUpdate 发生。如果您是一个初学者,似乎在何时使用哪一个的问题上感到困惑,事实上,直线运动适用于各种函数。但您要按照这个规则才能获得更好的视觉效果。

碰撞检测一个对象可从 RigidBody 组件中获得其质量,但您还需要告诉 Unity 如何处理与此对象的碰撞。虽然缩放对对象本身物理特性会产生影响,但您的作品或模型的大小和形状在这里并不重要。重要的是发生碰撞的组件的大小和形状,简单来说是一个您想让 Unity 检测另一个对象接触的在本对象的附近、表面或内部的定义区域。这是实现场景的方法,比如当您进入有僵尸闲逛的区域,或进入从山一侧弹跳下来巨石的区域就会执行检测。

有形状各异的碰撞体。2D 碰撞体可以是圆形体、边缘体、多边体或盒体。盒状碰撞体适用于形状像正方形或长方形的对象,或者您只是想检测发生在正方形区域中的碰撞。想象您能站在上面的平台 - 这是盒状碰撞体的一个很好的示例。将这个组件添加到您的游戏对象,您就能使用物理碰撞了。在图 6 中,我将一个圆形碰撞体和一个刚体添加到角色中,将一个盒状碰撞体添加到平台中。当我在编辑器中单击播放时,玩家立刻下降到平台上,然后停止。无需代码。

添加碰撞体
图 6 添加碰撞体

您可以通过更改碰撞体组件的属性来移动和调整该碰撞体覆盖的区域大小。默认情况下,含碰撞体的对象不会彼此穿过(触发器例外,我会在下节中进行介绍)。碰撞需要两个游戏对象上都有碰撞体组件,至少其中的一个对象必须有一个 RigidBody 组件,除非它是一个触发器。

如果我想在第一次发生此碰撞实例时让 Unity 调用我的代码,我只需通过一个脚本组件将以下代码添加到游戏对象(在之前的文章中讨论过):

void OnCollisionEnter2D(Collision2D collision)
{
  // If you want to check who you collided with,
  // you should typically use tags, not names.
  if (collision.gameObject.tag == "Platform")
  {
    // Play footsteps or a landing sound.
  }
}

触发器有时您想要检测碰撞,但同时不想卷入任何物理特性。想象在游戏中类似捡宝的场景。您不想在玩家接近时,在其正前方将硬币踢出去;您想让硬币被拾起,同时不干影响玩家移动。在这种情况下,您使用一个称为触发器的碰撞体,这无非是勾选了 IsTrigger 复选框的碰撞体。这将关闭物理特性,当对象 A(包含碰撞体)进入对象 B(也包含碰撞体)的区域内时,Unity 只会调用您的代码。在这种情况下,代码方法是 OnTriggerEnter2D 而不是 OnCollisionEnter2D:

void OnTriggerEnter2D(Collider2D collider)
{
  // If the player hits the trigger.
  if (collider.gameObject.tag == "Player")
  {
    // More on game controller shortly.
    GameController.Score++;
    // You don’t do: Destroy(this); because 'this'
    // is a script component on a game object so you use
    // this.gameObject or just gameObject to destroy the object.
    Destroy(gameObject);
  }
}

要记住的一点是,对于触发器,没有物理作用,它基本上只是一个通知。触发器也不需要游戏对象具备 Rigidbody 组件,或者说,因为不需要进行力计算。

一件经常难倒新开发人员的事就是当您将碰撞体添加到刚体上时,这些刚体的行为。如果我的对象具有一个圆形碰撞体,我将此对象放在一个斜面上(注意其碰撞体的形状),让它开始滚动,如图 7 所示。您在物质世界中看到的此模型是否是被设置在斜面上的一个滚轮。我之所以不将盒状碰撞体用于我的角色是因为当在其他碰撞体的边缘移动时,由于盒子的边有可能被拖住,由此会产生不很流畅的体验。而圆形碰撞体可以使这更顺畅。然而,对于不接受平稳旋转的情况,您可以使用 Rigidbody 组件的固定角度设置。

使用圆形碰撞体做平滑移动
图 7 使用圆形碰撞体做平滑移动

音频为了听到声音,你需要一个音频侦听器组件,默认情况下,已经存在于任何照相机上。若要播放声音,只要将音频源组件添加到游戏对象,并设置音频剪辑即可。Unity 支持大多数主流音频格式,且可以将较长的剪辑编码成 MP3。如果您有许多音频源,它们的剪辑已在 Unity 编辑器中进行了指定,请记住在运行时它们都会被加载。另一方面,您也可以通过位于特定资源文件夹中的代码加载音频,当完成时,再将它销毁。

当我将音频导入到我的项目中时,我将它保存为 WAV 文件,它是未压缩的音频。Unity 将重新编码更长的音频来优化它,所以始终使用您最优质的音频。这对短文件尤其如此,比如,Unity 不对声音效果进行编码。我还将音频源组件添加到我的主照相机中,虽然我可能已经把它添加到其他的游戏对象中。然后,我将 Adventure 音频剪辑指定到这个音频源组件中,并勾选了循环,因此它可以不断地循环。通过三个简单的步骤,现在我在玩游戏时可以播放背景音乐。

GUI/平视显示在一个游戏中,GUI 系统由好多东西构成。它可能涉及到菜单系统、运行状况及得分显示、武器库存,等等。通常情况下,GUI 系统是无论照相机在看哪儿(虽然这不是必要的),您都能在屏幕上看到的原地不动的内容。Unity 的 GUI 功能目前正在全面修订,新的 uGUI 系统将出现在 Unity 4.6 中。因为还没有发布,我在这里将简单地讨论一些基本功能,若要了解有关新的 GUI 系统的详细信息,可以查阅我的 Channel9 博客channel9.msdn.com/Blogs/AdamTuliper

为了将简单的显示文本添加到屏幕(例如,得分:0),请单击“游戏对象|创建其他| GUI文本”。此选项不再出现在 Unity 4.6 中,所以您要观看我提到过的有关 uGUI 的视频。在 4.6 版中,您仍然可以通过单击“添加组件”按钮将 GUI 文本组件添加到游戏对象上;它只是从编辑器菜单中消失了而已。通过现有的(旧版)Unity GUI 系统,您无法在场景视图中看到您的 GUI 对象,只有在游戏视图中才能看到,这使得布局创建有些奇怪。如果您喜欢,您可以用纯代码来设置您的 GUI,而且 GUILayout 类可以让您自动跟踪小部件。但我更喜欢使用 GUI 系统,因为我可以在其中执行单击和拖动操作,工作起来更轻松,这就是我觉得 uGUI 更为优越的原因。(在 uGUI 之前,这一领域中的主导产品是一个叫 NGUI 的相当可靠的第三方产品,它实际上被用作 uGUI 的初始代码库。)

更新该显示文本的最简单方法就是在编辑器中搜索或指定对 GUI 文本游戏对象的引用,并把它当作 .NET 中的标签并更新其文本属性。这使得更新屏幕 GUI 文本变得非常简单:

void UpdateScore()
{
  var score = GameObject.Find("Score").GetComponent<GUIText>();
  score.text = "Score: 0";
}

这是一个稍微缩短了的示例。对于性能,在 Start 方法中,我其实是要缓存对 GUIText 组件的引用,以便在调用每个方法时都不对其进行查询。

得分跟踪跟踪得分很容易。您只需要有一个类,能够显示公共方法或设定分数的属性。在游戏中将充当游戏组织者的对象称为“游戏控制器”很常见。游戏控制器可以负责触发游戏保存、加载、记分等。在这个示例中,我可以有一个类,能够显示分数变量,如图 8 所示。我将这个组件分配给一个空的游戏对象,以便在场景加载时使用。当更新得分时,GUI 被依次更新。在 Unity 编辑器中,对 _scoreText 变量进行指定。只需将任何 GUIText 游戏对象拖放到这个显示字段上,或使用搜索小部件,其中该脚本组件在编辑器中显示分数文本变量。

图 8 创建 _scoreText 变量

public class GameController : MonoBehaviour
{
  private int _score;
  // Drag a GuiText game object in the editor onto
  // this exposed field in the Editor or search for it upon startup
  // as done in Figure 12.
  [SerializeField]
  private GUIText _scoreText;
  void Start()
  {
    if (_scoreText == null)
    {
      Debug.LogError("Missing the GuiText reference. ");
    }
  }
  public int Score
  {
    get { return _score; }
    set
    {
      _score = value;
        // Update the score on the screen
      _scoreText.text = string.Format("Score: {0}", _score);
    }
  }
}

然后,我可以按如下方法简单地更新(在本例中)蘑菇的触发器代码,以在每次拾取时增加得分:

void OnTriggerEnter2D(Collider2D collider)
{
  if (collider.gameObject.tag == "Player")
  {
    GameController.Score++;
    Destroy(gameObject);
  }
}

动画正如 XAML 一样,通过在关键帧中执行各种操作来创建动画。我可以轻松地用整篇文章来介绍 Unity 中的动画,但由于空间有限,我将在此简要说明。Unity 有两种动画系统,旧系统和最新的 Mecanim 系统。旧系统使用动画 (.ani) 文件,而 Mecanim 使用状态来控制动画文件的播放。

在默认情况下,2D 动画使用 Mecanim。制作动画的最简单的方法是将图片拖放到您的场景中,让 Unity 为您创建动画。开始时,我将一些单个 sprite 拖动到 Unity 中,接下来 Unity 为我创建一些东西。首先,它使用用来绘制 sprite 的 sprite 渲染器组件来创建游戏对象。然后,它会创建一个动画文件。您可以通过“窗口|动画”和突出显示自己的游戏对象来进行查看。动画器会显示分配的动画文件,就我的情况而言,包含六个关键帧,因为我将六幅图像放进了我的场景中。每个关键帧控制组件上一个或多个参数;此处,它更改了 Sprite 渲染器组件的 Sprite 属性。动画只不过是以某个使眼睛能够感知运动的速度显示的一些单个图像。

接下来,Unity 在游戏对象上创建动画器组件,如图 9 所示。

指向控制器的动画器组件
图 9 指向控制器的动画器组件

该组件指向一个称为动画控制器的简单状态机。这是一个由 Unity 创建的文件,它只是显示了默认状态;换句话说,它总是处于“空闲”状态,而这是也是唯一可用的状态。这种空闲状态除了指向我的动画文件之外,没有其他作用。图 10 显示了在时间线上的实际关键帧数据。

空闲的动画数据
图 10 空闲的动画数据

只是要播放动画,但是看上去似乎有很多事情要做。尽管状态机的功能就是使您可以通过设置简单的变量来控制它们。但请记住,状态只会指向动画文件(虽然在 3D 中,您可以别出心裁地做一些将动画融合在一起之类的事情)。

然后,我将用更多的图像来制作跑动的动画,并将它们拖放到我的 Yeti 游戏对象上。因为我已经将动画器组件添加到游戏对象上,所以 Unity 只创建一个新的动画文件,并添加一个称为“跑动”的新状态。我只要在空闲状态上单击鼠标右键,创建一个过渡来跑动。这将在空闲状态和跑动状态之间创建一个箭头。那么我可以添加一个名为“Running”的新变量,它简单易用,您只需在状态之间的箭头上单击,并更改使用变量的条件,如图 11 所示。

从“空闲”状态更改为“跑动”状态
图 11 从“空闲”状态更改为“跑动”状态

当“Running”为 true 时,空闲动画状态会更改为跑动动画状态,这仅仅意味着跑动动画文件在播放。您可以轻松控制代码中的这些变量。如果您想在单击鼠标按钮时通过触发跑动状态来启动跑动动画,您可以添加图 12 中显示的代码。

图 12 使用代码来更改状态

private Animator _animator;
void Awake()
{
    // Cache a reference to the Animator 
    // component from this game object
    _animator = GetComponent<Animator>();
}
void Update()
{
  if (Input.GetButtonDown("Fire1"))
  {
    // This will cause the animation controller to
    // transition from idle to run states 
    _animator.SetBool("Running", true);
  }
}

在我的示例中,我使用单个 sprite 来创建动画。虽然,使用 sprite 表(一个单独的图像文件,里面含有多个图像)是很常见的。Unity 支持 sprite 表,所以问题在于告诉 Unity 如何分割您的 sprite,然后将这些片段拖放到场景中。有所不同的几个步骤分别为在 Sprite 属性中将 Sprite 模式由单一更改为多个和打开 Sprite 编辑器,这样可以自动分割 sprite 并应用所做的更改,如图 13 所示。最后,展开 sprite(点击项目视图中的 sprite 图标上的小箭头),突出显示所产生的 sprite,像之前那样将它们拖放到场景中。

创建 Sprite 表
图 13 创建 Sprite 表

在您学会这个系统之前,动画一直都可能是一个很复杂的课题。若要了解详细信息,请查看我的 Channel9 博客或 Unity 学习网站上的优秀资源。

关卡末尾当玩家玩到关卡末尾时,您可以简单将碰撞体设置为触发器,并允许玩家击打该区域。当他这样做时,您只需加载另一关卡或重新加载当前关卡:

void OnTriggerEnter2D(Collider2D collider)
{
  // If the player hits the trigger.
  if (collider.gameObject.tag == "Player")
  {
    // Reload the current level.
    Application.LoadLevel(Application.loadedLevel);
    // Could instead pass in the name of a scene to load:
    // Application.LoadLevel("Level2");
  }
}

游戏对象及其相应的属性显示在图 14 中。注意,碰撞体的高度要足够高,以至于玩家不能跳过去,并且要将此碰撞体设置为触发器。

游戏对象及其属性
图 14 游戏对象及其属性

游戏玩法在像这样的一个简单的 2D 游戏中,流程是非常简单的。玩家开始。刚体受到的重力会使玩家下落。玩家和平台上设置了碰撞体,所以玩家会停止。读取键盘、鼠标和触摸输入并移动玩家。玩家通过应用使其能够跳跃的 rigidbody.AddForce 在平台之间进行跳跃,通过读取 Input.GetAxis(“水平”),并将其应用到 rigidbody.velocity 来进行向左移动或向右移动。玩家拾起蘑菇(正是被设置为触发器的碰撞体)。当玩家碰触到它们,可以增加得分,同时也会自我毁灭。当玩家终于成功到达最后一个标志时,会有一个碰撞体/触发器重新加载当前关卡。此处,另一个待办事项是在地面下添加一个大型碰撞体来检测玩家掉落平台的时刻,然后再重新加载该关卡。

预设在编码和设计中,重复使用都是很重要的。当您指定一些组件并且自定义您的游戏对象之后,总是要在相同的场景中,甚至多个场景或游戏中重复使用它们。您可以在场景中创建某个游戏对象的另一个实例,但您也可以创建一个并不存在于场景中的预设实例。考虑平台及其碰撞体。如果您想在整个场景中重复使用它们,目前还不可以。但是,通过创建预设,就能做到。只需将任何游戏对象从层次结构中拖回到项目文件夹中,然后创建一个扩展名为 .prefab 的新文件,并且该新文件包括所有子层次结构。现在,您可以将这个文件拖放到您的场景中并重新使用它。原来的游戏对象会变成蓝色,表明它现在已连接到预设。更新 .prefab 文件可以更新场景中的所有实例,您也可以将更改从经过修改的场景预设推送回到 .prefab 文件中。

单击预设可显示里面包含的游戏对象,如图 15 所示。如果在此处进行更改,场景中的所有实例都将被更新。

查看预设内容
图 15 查看预设内容

总结

还有一些在整个游戏中执行的常用操作。在本文中,我介绍了使用碰撞体、刚体、动画、得分记录、基本的 GUI 文本以及读取用户输入以使用力来移动玩家的游戏平台的基础知识。这些构建的模块可以在各种游戏类型中重复使用。请在我的下一篇文章中继续关注 3D 讨论话题!

其他学习

 


Adam Tuliper 是生活在阳光明媚的加利福尼亚州南部的一位 Microsoft 资深技术传播者。他是一位独立的游戏开发人员,Orange County Unity Meetup(奥兰治县 Unity 聚会)的共同管理者,以及 pluralsight.com 的作者。他和他的妻子即将拥有自己的第三个孩子,所以在他尚有闲暇的时间里,您可以通过访问 adamt@microsoft.com 或 Twitter twitter.com/AdamTuliper 来联系到他。

衷心感谢以下技术专家对本文的审阅:Matt Newman (Subscience Studios)、Tautvydas Žilys (Unity)