UNITY 3D – 游戏编程 – 第 6 部分






4.94/5 (18投票s)
本系列第六篇文章,将讨论 Unity 3D 以及如何开始自己的 3D 项目。
引言
在本系列的第六部分,我们将扩展我们在系列第五部分开始的游戏创意。
如果您还没有阅读,请花点时间阅读
- Unity 3D – 游戏编程 – 第 1 部分
-
Unity 3D – 游戏编程 – 第 6 部分
Unity 3D 网络文章
Unity 3D Leap Motion 和 Oculus Rift 文章
在本系列的第一部分,我们从 Unity 3D 环境的最基本知识开始。了解 IDE 和将在项目中使用的各个部分。我们还涵盖了如何在设计器中使用工具对选定的游戏对象应用不同的变换:定位、旋转和缩放。最后,我们研究了如何创建我们的第一个脚本,并使用该脚本在立方体的 Y 轴上应用旋转变换。
在本系列的第二部分中,我们更多地研究了通过编码对给定对象进行变换。我们还研究了如何创建对场景中对象的渲染至关重要的光源。
在本系列的第三部分中,我们研究了如何通过键盘处理用户输入,并根据键码采取特定操作。
在本系列的第四部分,我们研究了创建简单的用户界面。我们开发的界面为用户提供了反馈机制,也为用户提供了输入到我们的游戏或模拟中的另一种方法。
在第五部分,我们提出了一个简单游戏的构想。我们还研究了如何将 3D 模型导入游戏引擎。
在第六部分,我们将扩展我们的游戏创意,使其更有趣、更完整。
Windows Phone 8.x 演示
我提供了一个免费的手机应用程序,您可以下载并在 Windows Phone 上预览演示。要下载该移动应用程序,请遵循以下链接:CodeProjectArticleSample
文章代码和视觉效果的实时预览
实时预览链接:http://www.noorcon.com/CodeProject/CodeProjectArticlePreview.html
背景
注意:对于本篇文章,我将使用 SketchUp 创建一些简单的积木,并将其导入 Unity!我不是 3D 建模师或设计师,所以请耐心等待并原谅我的不足!
假定本文的读者对编程概念总体上是熟悉的。也假定读者对 C# 语言有理解和经验。还建议文章读者熟悉面向对象编程和设计概念。我们将在文章中简要介绍它们,但不会深入细节,因为它们是完全独立的主题。我们还假定您有学习 3D 编程的热情,并具备 3D 图形和向量数学的基本理论概念。
最后,本文使用 Unity 3D 版本 4.6.1,这是截至首次发布日期时最新的公开版本。系列中讨论的大多数主题都将兼容游戏引擎的旧版本,甚至可能兼容今年将发布的版本。然而,有一个主题在当前 4.6.1 版本与游戏引擎的旧版本相比有显著不同,那就是 UI(用户界面)管道。这是因为引擎中新的 UI 架构远优于我们在此版本发布之前的架构。我个人对新的 UI 架构非常满意。
使用代码
下载文章系列的工程/源代码:下载源代码。
随着后续文章的提交,项目/源代码也将随之扩展。新的项目文件和源文件将包含系列中较旧的部分。
注意:要获取最新代码,请转到本系列最新发布的部分并下载代码。
回顾构思
如果您还记得第五部分,我们开始创建一个类似于迷宫的关卡。该关卡必须足够大,以便我们能够自由移动并与其他对象进行交互。您可以回到第五部分回顾摘要。
我使用 SketchUp 创建了以下模型,该模型用作我们的关卡基础。
图 1-我的关卡 SketchUp 模型
然后,我们引入了由 **Sphere** 原始体表示的 **角色玩家 (CP)**,以及由修改后的 **Capsule** 原始体表示的 **硬币** 对象。
我们使 **CP** 可移动,并将其转换为刚体,以便我们可以使用游戏引擎提供的物理引擎和碰撞检测。我们还使 **硬币** 对象的 **Collider** 组件成为触发器。
实现游戏
从摘要中,我们了解到我们希望在给定的时间范围内尽可能多地收集硬币。到目前为止,我们已经创建了游戏所需的主要 GameObjects。现在我们需要开始考虑游戏的逻辑。
思考游戏设计和游戏玩法
每款游戏都有其规则。这些规则由游戏的创建者/开发者/架构师定义和创建。规则越复杂,设计和实现就越复杂。
在我们的例子中,我们面前有一个非常简单的游戏,但尽管如此,我们仍然需要考虑、定义和创建一套规则。例如,让我们看以下问题:
-
您的硬币将如何放置在关卡中?
-
它们将在设计时手动放置?还是
-
它们将在运行时动态放置?
-
-
它们会有生命周期吗?也就是说
-
它们将在游戏的整个过程中可用,还是直到被玩家拾取?或者
-
它们会根据某些逻辑出现和消失?
-
如果它们消失了,它们将如何重新出现,或者它们会重新出现吗?
-
-
-
每枚硬币都会有完全相同的价值吗?
-
您的玩家将如何与硬币互动?
-
您将如何计分?
-
-
您的玩家将如何与其他关卡中的对象互动?(如果有)
-
您的 UI 将向玩家显示什么?
-
收集的硬币数量?
-
游戏的当前运行时间?
-
完成的时间?
-
让我们继续尝试回答其中的一些问题,并开始着手在我们的游戏中实现它们。
为了简单起见,让我们决定在设计时将硬币放置在关卡中。我们还可以为硬币分配一个随机值,使其在游戏中更有趣。我们可以编程使硬币具有 1 到 9 之间的随机值。目前,我们还可以决定硬币在整个游戏过程中都具有完整的生命周期。
CP 将通过碰撞与硬币对象进行交互。当玩家与硬币对象碰撞时,它将触发一个事件,该事件将拾取硬币并执行必要的计算来增加玩家在游戏中的分数。
玩家将有有限的时间来尽可能多地拾取硬币。
实现我们的游戏玩法
让我们继续创建将为我们的硬币赋予生命的脚本。我们称之为 _Coin.cs_
using UnityEngine; using System.Collections; public class Coin : MonoBehaviour { private int value; public int VALUE { get{ return this.value; } } // Use this for initialization void Start () { this.value = Random.Range (1, 9); } // Update is called once per frame void Update () { } }
此脚本在 _Coin.cs_ 中定义,将附加到 **硬币** 对象。在游戏开始时,它将随机生成一个 1 到 9 之间的数字,并将其放入 value 变量中。这将是硬币在整个游戏中的价值。
预制件
通过向场景添加 GameObject,添加组件并将其属性设置为适当的值,可以方便地构建 GameObject。但是,当您有一个像 NPC、道具或场景碎片这样的对象,并且该对象在场景中被重复使用多次时,这可能会导致问题。简单地复制对象肯定会产生副本,但它们都将独立可编辑。通常,您希望某个特定对象的所有实例都具有相同的属性,因此当您在场景中编辑一个对象时,您不希望重复地对所有副本进行相同的编辑。
Prefab 正是这样做的,由于我们需要创建 **Coin** GameObject 的多个实例,因此我们希望创建一个 **Coin** prefab。有几种创建 **Prefab** 的方法,我将向您展示一种非常简单的方法。
在 **Project Window** 中,选择您的 _prefab folder_。将您的 **coin** GameObject 从 **Hierarchy Window** 拖放到 **Project Window** 的 _prefab folder_ 下。
图 2-显示创建 Coin GameObject 的 Prefab
Prefab 非常有用,我们将在更复杂的场景中更有效地使用它们。在这个阶段,我们只是想复制我们的对象,并能够随时通过设计器或代码创建该对象的实例!
让我们使用新创建的 Prefab 在关卡中放置一些硬币。我在场景中放置了大约八枚硬币,并尽量将它们分散在网格上。下图是我现在的样子
图 3-场景中显示 8 个 Prefab 硬币
这些对于演示目的来说已经足够了。现在,让我们看看需要应用于 **Character Player (CP)** 的代码,以检测与 **Coin** 对象的碰撞。
这是 CP 对象到目前为止的列表
using UnityEngine; using UnityEngine.UI; using System.Collections; public class playerInput : MonoBehaviour { private int score; // internal score variable public int SCORE { get{ return this.score; } } // Use this for initialization void Start () { this.score = 0; } // Update is called once per frame void Update () { // code for the movement of player (CP) forward if(Input.GetKey(KeyCode.UpArrow)){ this.transform.Translate(Vector3.forward * Time.deltaTime); } // code for the movement of player (CP) backward if(Input.GetKey(KeyCode.DownArrow)){ this.transform.Translate(Vector3.back * Time.deltaTime); } // code for the movement of player (CP) left if(Input.GetKey(KeyCode.LeftArrow)){ this.transform.Rotate(Vector3.up, -5); } // code for the movement of player (CP) right if(Input.GetKey(KeyCode.RightArrow)){ this.transform.Rotate(Vector3.up, 5); } } // This event will be raised by object that have their Is Trigger attributed enabled. // In our case, the coin GameObject has Is Trigger set to true on its collider. void OnTriggerEnter(Collider c){ if(c.tag.Equals("coin")){ Coin coin = c.GetComponent<Coin>(); // increase score this.score += coin.VALUE; // remove the Coin object from the scene Destroy(c.gameObject); } } }
新的修改做了几件事。首先,我们引入了一个变量来跟踪玩家的分数。然后在 **OnTriggerEnter(Collider c)** 函数中,我们检查是否与硬币对象发生碰撞,如果是,我们获取 **Coin** 对象的 **Coin Script Component** 并通过 **VALUE** 属性提取硬币的值。然后我们将分数增加硬币的值。然后我们想从场景中移除 Coin 对象,因此我们使用 **Destroy(c.gameObject)** 函数从场景中销毁对象。
如果您一直按照说明操作,运行程序时您将看到结果。
思考用户界面
游戏关卡的用户界面非常简单。我们只需要能够显示玩家的分数和计时器,以指示玩家还有多少时间来达成他的/她的目标。
我将不介绍创建 UI 的步骤,因为我们将系列中的第四部分专门用于涵盖 UI 创建的基础知识。
首先,我想解决游戏的得分 UI。下图显示了它的外观
图 4-得分 UI
要创建得分 UI,我使用了一个固定在屏幕左上角的 **Panel**,面板内有一个显示硬币图标的 **Image**,以及一个用于标签的 **Text** 元素。要获得自定义的外观和感觉,您可以为面板背景等应用纹理……有关详细信息,请参阅第四部分。
现在,让我们为计时器做类似的事情。对于计时器,我使用了一个固定在屏幕右上角的新 **Panel**,以及一个显示计时器的 **Text** 元素。
目前我对我为这款特定游戏设计的 UI 感到非常满意。现在,我们需要确保我们的代码可以访问 UI 元素,并相应地更新它们。
我已将 _playerInput.cs_ 脚本更新为以下内容
using UnityEngine; using UnityEngine.UI; using System.Collections; public class playerInput : MonoBehaviour { public Text lblScore; // text UI element for displaying score public Text lblTimer; // text UI element for displaying timer private int score; // internal score variable public int SCORE { get{ return this.score; } } private float timer; // variable to keep track of time // Use this for initialization void Start () { this.score = 0; this.timer = 30.00f; // check to make sure labels are defined before updating if (this.lblScore != null) this.lblScore.text = this.score.ToString(); if (this.lblTimer != null) this.lblTimer.text = string.Format("{0:F2}", this.timer - Time.time); } // Update is called once per frame void Update () { if (this.lblTimer != null) this.lblTimer.text = string.Format("{0:F2}", this.timer - Time.time); // code for the movement of player (CP) forward if(Input.GetKey(KeyCode.UpArrow)){ this.transform.Translate(Vector3.forward * Time.deltaTime); } // code for the movement of player (CP) backward if(Input.GetKey(KeyCode.DownArrow)){ this.transform.Translate(Vector3.back * Time.deltaTime); } // code for the movement of player (CP) left if(Input.GetKey(KeyCode.LeftArrow)){ this.transform.Rotate(Vector3.up, -5); } // code for the movement of player (CP) right if(Input.GetKey(KeyCode.RightArrow)){ this.transform.Rotate(Vector3.up, 5); } } // This event will be raised by object that have their Is Trigger attributed enabled. // In our case, the coin GameObject has Is Trigger set to true on its collider. void OnTriggerEnter(Collider c){ if(c.tag.Equals("coin")){ Coin coin = c.GetComponent<Coin>(); // increase score this.score += coin.VALUE; // update score on the UI if (this.lblScore != null) this.lblScore.text = this.score.ToString(); // remove the Coin object from the scene Destroy(c.gameObject); } } }
我们在脚本中引入了几个变量:_lblScore_、_lblTimer_ 和 _timer_。我们更新了 **Start()** 函数来执行以下操作:初始化 **score** 和 **timer** 的默认值,检查 _lblScore_ 和 _lblTimer_ 是否不为空,如果不为空,则将其值分配给它们的文本属性。
我们更新的下一个函数是 **Update()** 函数。我们只包含了几行来为我们更新计时器字段。
最后一个更新的函数是 **OnTriggerEnter(Collider c)** 函数。在检测到我们与 **Coin** 对象碰撞后,我们提取硬币的值并相应地增加玩家的分数。最后,_lblScore_ 会更新以在用户界面上相应地反映分数。
稍微增强一下逻辑
在结束本系列第六部分之前,我还有一些其他的增强功能想要实现。如果您注意到,我们没有结束游戏的方式!好吧,如您所知,每款游戏都必须有结束的时候。因此,我们应该考虑如何触发游戏结束!
为了解决这个难题,我们将实现以下逻辑来为游戏玩法画上句号:
-
当计时器为 0.00 时,游戏应停止。
-
当玩家收集关卡中的所有硬币时,游戏应停止。
游戏结束时,我们需要向用户显示一个提示,显示他们的分数,并让他们能够开始新游戏或结束游戏。
注意: 应用程序的退出必须根据游戏部署的平台来定义!Web 应用程序没有所谓的应用程序退出!
以下代码列表显示了我们将如何修改我们的代码以处理情况 (1) 和情况 (2) 来触发游戏结束。
using UnityEngine; using UnityEngine.UI; using System.Collections; public class playerInput : MonoBehaviour { public Text lblScore; // text UI element for displaying score public Text lblTimer; // text UI element for displaying timer private int score; // internal score variable public int SCORE { get{ return this.score; } } private float levelTime; // variable holing time to complete level private float timeLeft; // variable for the actual timer count down public bool END_GAME; // variable indicating end of game public int numOfCoinsInLevel; // will be initialized at the Start of the game public int numOfCoinsCollected; // will be incremented each time we collect a coin // Use this for initialization void Start () { this.score = 0; this.timer = 60.00f; this.numOfCoinsCollected = 0; this.END_GAME = false; // check to make sure labels are defined before updating if (this.lblScore != null) this.lblScore.text = this.score.ToString(); if (this.lblTimer != null) this.lblTimer.text = string.Format("{0:F2}", this.timer - Time.time); // get number of coins in the scene at the start of the game this.numOfCoinsInLevel = GameObject.FindGameObjectsWithTag ("coin").Length; } // Update is called once per frame void Update () { if (!this.END_GAME) { // compute time left this.timeLeft = this.timer - Time.time; // update UI label for timer if (this.lblTimer != null){ this.lblTimer.text = string.Format("{0:F2}", this.timeLeft); } // check to see if we need to end the game based on the timer if(this.timeLeft<=0.00f || this.numOfCoinsInLevel<=this.numOfCoinsCollected){ this.END_GAME = true; if (this.lblTimer != null){ this.lblTimer.text = string.Format("{0:F2}", 0.00f); } } // code for the movement of player (CP) forward if(Input.GetKey(KeyCode.UpArrow)){ this.transform.Translate(Vector3.forward * Time.deltaTime); } // code for the movement of player (CP) backward if(Input.GetKey(KeyCode.DownArrow)){ this.transform.Translate(Vector3.back * Time.deltaTime); } // code for the movement of player (CP) left if(Input.GetKey(KeyCode.LeftArrow)){ this.transform.Rotate(Vector3.up, -5); } // code for the movement of player (CP) right if(Input.GetKey(KeyCode.RightArrow)){ this.transform.Rotate(Vector3.up, 5); } }else{ Debug.Log("GAME ENDED!!!"); } } // This event will be raised by object that have their Is Trigger attributed enabled. // In our case, the coin GameObject has Is Trigger set to true on its collider. void OnTriggerEnter(Collider c){ if(c.tag.Equals("coin")){ Coin coin = c.GetComponent<Coin>(); // increase score this.score += coin.VALUE; this.numOfCoinsCollected += 1; // update score on the UI if (this.lblScore != null) this.lblScore.text = this.score.ToString(); // remove the Coin object from the scene Destroy(c.gameObject); } } }
查看新列表,您会注意到代码中有很多变化。没有什么主要的或复杂的,但尽管如此,我们的代码现在内容更丰富了。
我们还引入了几个变量来跟踪我们游戏中的统计数据。引入了一个新变量 _timeLeft_ 用于计时器,以处理时间到了何时终止游戏!引入了一个布尔变量 _END_GAME_ 作为标志,以确定游戏是继续还是结束。两个变量 _numOfCoinsInLevel_ 和 _numOfCoinsCollected_ 用于确定如果玩家在时间用完之前收集了关卡中的所有硬币,游戏是否应该结束。
下一步是创建将在游戏结束时显示的 UI。同样,这里没什么花哨的,但我们会尝试制作一些令人愉悦的东西。下图将显示我们的 End Game 面板的外观。
图 5-游戏结束画布显示
一如既往,我们需要更新我们的脚本来处理新的增强功能。这是 _playerInput.cs_ 脚本的更新列表。
using UnityEngine; using UnityEngine.UI; using System.Collections; public class playerInput : MonoBehaviour { public Text lblScore; // text UI element for displaying score public Text lblTimer; // text UI element for displaying timer public Canvas endGameCanvas; // Canvas holding UI elements for End of Game public Text lblEndOfGameScore; public Text lblEndOfGameTime; public Text lblEndOfGameCoinCont; private int score; // internal score variable public int SCORE { get{ return this.score; } } private float levelTime; // variable holing time to complete level private float timeLeft; // variable for the actual timer count down public bool END_GAME; // variable indicating end of game public int numOfCoinsInLevel; // will be initialized at the Start of the game public int numOfCoinsCollected; // will be incremented each time we collect a coin // Use this for initialization void Start () { this.score = 0; this.levelTime = Time.time + 30.00f; this.numOfCoinsCollected = 0; this.END_GAME = false; this.endGameCanvas.gameObject.SetActive(false); // check to make sure labels are defined before updating if (this.lblScore != null) this.lblScore.text = this.score.ToString(); if (this.lblTimer != null) this.lblTimer.text = string.Format("{0:F2}", this.levelTime - Time.time); // get number of coins in the scene at the start of the game this.numOfCoinsInLevel = GameObject.FindGameObjectsWithTag ("coin").Length; } // Update is called once per frame void Update () { if (!this.END_GAME) { // compute time left this.timeLeft = this.levelTime - Time.time; // update UI label for timer if (this.lblTimer != null){ this.lblTimer.text = string.Format("{0:F2}", this.timeLeft); } // check to see if we need to end the game based on the timer if(this.timeLeft<=0.00f || this.numOfCoinsInLevel<=this.numOfCoinsCollected){ this.END_GAME = true; if (this.lblTimer != null && this.lblEndOfGameTime != null){ if(this.timeLeft>=0.00f){ this.lblTimer.text = string.Format("{0:F2}", this.timeLeft); this.lblEndOfGameTime.text = string.Format("{0:F2}", this.timeLeft); }else{ // this else block is written to ensure that if the timer is up, we always get 0.00 // and not positive or negative values, i.e. 0.01, or -0.01 and etc... this.lblTimer.text = string.Format("{0:F2}", 0.00f); this.lblEndOfGameTime.text = string.Format("{0:F2}", 0.00f); } } if(this.lblEndOfGameScore != null && this.lblEndOfGameCoinCont != null){ this.lblEndOfGameScore.text = this.SCORE.ToString(); this.lblEndOfGameCoinCont.text = this.numOfCoinsCollected.ToString(); } } // code for the movement of player (CP) forward if(Input.GetKey(KeyCode.UpArrow)){ this.transform.Translate(Vector3.forward * Time.deltaTime); } // code for the movement of player (CP) backward if(Input.GetKey(KeyCode.DownArrow)){ this.transform.Translate(Vector3.back * Time.deltaTime); } // code for the movement of player (CP) left if(Input.GetKey(KeyCode.LeftArrow)){ this.transform.Rotate(Vector3.up, -5); } // code for the movement of player (CP) right if(Input.GetKey(KeyCode.RightArrow)){ this.transform.Rotate(Vector3.up, 5); } }else{ this.endGameCanvas.gameObject.SetActive(true); } } // This event will be raised by object that have their Is Trigger attributed enabled. // In our case, the coin GameObject has Is Trigger set to true on its collider. void OnTriggerEnter(Collider c){ if(c.tag.Equals("coin")){ Coin coin = c.GetComponent<Coin>(); // increase score this.score += coin.VALUE; this.numOfCoinsCollected += 1; // update score on the UI if (this.lblScore != null) this.lblScore.text = this.score.ToString(); // remove the Coin object from the scene Destroy(c.gameObject); } } public void butPlayAgain_Click(){ Start (); } }
关注点
正如您所见,事情变得相当复杂!而且还有一些我们没有考虑到的场景。假设以下情况,比如玩家在游戏过程中收集了场景中的所有硬币,在游戏结束时,他们可以选择再次玩。但这里有一个我们尚未处理的问题!您能告诉我问题是什么吗?在继续阅读下一段之前,请先思考一下。
问题在于,我们在设计时将硬币放置在了场景中!所以当我们收集硬币时,我们实际上是在运行时将它们从内存中移除!您看到问题了吗?嗯,问题在于,目前我们无法在场景中重新创建这些硬币,所以当游戏再次开始时,它会立即停止,因为硬币数量为零!我们将在第七部分尝试解决这个问题。
注意: 请注意,我没有详细介绍 UI 的制作。该主题已在第四部分中介绍。
历史
这是我将逐渐为 Code Project 社区贡献的系列文章中的第六篇。
- Unity 3D – 游戏编程 – 第 1 部分
-
Unity 3D – 游戏编程 – 第 6 部分
Unity 3D 网络文章
Unity 3D Leap Motion 和 Oculus Rift 文章