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






4.90/5 (16投票s)
本系列第七篇文章,将讨论Unity 3D以及如何开始你自己的3D项目。
引言
在本系列的第七部分,我们将扩展我们在第5部分和第6部分中提出的游戏构思。
如果您还没有阅读,请花点时间阅读
- Unity 3D – 游戏编程 – 第 1 部分
-
Unity 3D – 游戏编程 – 第 7 部分
Unity 3D 网络文章
Unity 3D Leap Motion 和 Oculus Rift 文章
在本系列的第一部分,我们从Unity 3D环境的最基本开始。我们熟悉了IDE和您将在整个项目中使用的各个部分。我们还介绍了如何使用设计器中的工具对选定的游戏对象应用不同的变换:定位、旋转和缩放。最后,我们研究了如何创建我们的第一个脚本,并使用该脚本在立方体的Y轴上应用旋转变换。
在本系列的第二部分中,我们更多地研究了通过编码对给定对象进行变换。我们还研究了如何创建对场景中对象的渲染至关重要的光源。
在本系列的第三部分中,我们研究了如何通过键盘处理用户输入,并根据键码采取特定操作。
在本系列的第四部分,我们研究了创建简单的用户界面。我们开发的界面为用户提供了反馈机制,也为用户提供了输入到我们的游戏或模拟中的另一种方法。
在第五部分,我们提出了一个简单游戏的构想。我们还研究了如何将 3D 模型导入游戏引擎。
在第六部分,我们扩展了我们的游戏构想,使其更有趣、更完整。我们引入了几个关于游戏玩法和游戏设计的概念,并为我们的游戏创建了一个简单的用户界面。
在第七部分,我们将回答在第6部分中提出的一些问题。
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架构非常满意。
使用代码
下载系列文章的项目/源代码:下载源文件。
随着后续文章的提交,项目/源代码也将随之扩展。新的项目文件和源文件将包含系列中较旧的部分。
注意:要获取最新代码,请转到本系列最新发布的部分并下载代码。
改进我们的设计
继续我们之前的工作,让我们来看看我们到目前为止的设计中的一些缺点。
当前设计的主要缺点之一如下:当一个给定的游戏完成后,玩家可以选择“重玩”。如果你遵循了前几篇文章中的结构和逻辑,你可能会注意到这里有一个大缺陷。我们当前的设计未能正确重置游戏以进行新会话。要做到这一点,我们需要进行一些设计更改,以便能够处理这个问题。
由于我们在设计时创建了金币,当我们在运行时收集金币时,实际上是从内存中移除了金币,我们没有其他方法可以在新游戏会话中重新生成它们。这是一个问题,因为当前的游戏逻辑基于两个条件结束游戏:
-
如果时间到了,游戏将结束。
-
如果在时间到了之前玩家收集了所有金币,游戏将结束。
在第一种情况下,假设时间到了,玩家已经收集了八个金币中的五个。当他或她选择“重玩”选项时,游戏将以新的计时器开始,但需要收集的金币总数将是原始游戏和新游戏的差值,因此新游戏会话将从需要收集三个金币开始。以此类推。
在第二种情况下,当玩家收集完所有金币时,游戏结束。当玩家选择“重玩”选项时,根据当前的实现,游戏将立即结束,因为没有金币了!
为了解决这个问题,我们需要重新思考和重新设计我们的游戏逻辑。
-
我们需要一个动态创建金币的机制。
同样,有不止一种设计选择和方法可用于处理此标准。我们可以
-
预定义关卡中所有可用金币的位置为固定。
-
或者让它们随机放置在关卡中。
从编码的角度来看,选项一会更简单。您将在设计时预定义金币的位置,并使用这些变换数据一次又一次地动态实例化相同位置的金币。缺点是,对于所有游戏会话,金币的位置将始终相同。
选项二将需要更多的规划和编码,因为您需要随机放置金币在关卡中,但是,您必须小心,要放置它们时具有一定的限制,以免它们生成在某个墙壁内等等……
让我们来实现选项二。这种方法将使游戏每次玩家玩时都更加随机,并且,实现选项二还将涵盖两个选项的概念。
随机化我们的金币
首先,我们需要确定金币的放置边界。在我们的关卡设计中,我们知道我们的关卡是一个10米x10米的地形。我们还知道关卡在该区域内有定义的墙壁。
如果我们的关卡放置在原点(0,0,0),那么我们的关卡在世界中的坐标将覆盖以下顶点:对于正Z方向:(5,0,5)和(-5,0,5);对于负Z方向,它将是(5,0,-5)和(-5,0,-5)。这四点是我们场景中定义的地形边界。
所以我们生成的任何金币都必须在我们空间中的这四个坐标内。但是,我们关卡中还有预定义的墙壁,我们也需要考虑进去。同样,这不是一项艰巨的任务,因为作为关卡的设计者,您确切地知道您是如何细分迷宫的,至少您应该知道!
图1-显示重要的关卡尺寸
在上图中,请注意我已标示了内部墙壁偏移的尺寸。使用此信息,您将能够编写代码,根据关卡设计有效地放置金币。最简单的分解方法是将区域分解。我将区域划分为四个主要区域。
图2-高亮显示定义的区域
正如你在图2中看到的,我标记了四个可用于放置金币的区域。最大的区域是区域1,它可以通过以下点定义:
-
区域1,外边界 (4.5,0,4.5) (-4.5,0,4.5) (4.5,0,-4.5) (-4.5,0,-4.5)
-
区域1,内边界 (3,0,3) (-3,0,3) (3,0,-3) (-3,0,-3)
区域2、3和4可以用以下点定义:
-
区域2 (2,0,2) (-2,0,2) (2,0,1.5) (-2,0,1.5)
-
区域3 (2,0,0.5) (-2,0,0.5) (2,0,-0.5) (-2,0,-0.5)
-
区域4 (2,0,-2) (-2,0,-2) (2,0,-1.5) (-2,0,-1.5)
注意:由于墙壁厚度为0.5,您需要将此考虑在边界内。
下一个问题是,我们希望每个区域生成多少金币。从我们定义的区域来看,区域1最大,因此可以容纳最多的金币。让我们为区域1分配八个金币。区域2、3和4具有相同的方形区域,并且相对较小,因此让我们为每个剩余区域分配两个金币。
现在让我们看看我们的脚本将根据新规则如何改变。我们将更新我们的inputPlayer.cs脚本,使其变为以下内容:
using UnityEngine; using UnityEngine.UI; using System.Collections; public class playerInput : MonoBehaviour { ... public GameObject coinPrefab; // variable used to store the coin prefab ... // Use this for initialization void Start () { #region COIN CREATION // we need to create the coins dynamically per region // Coins for Region 1 for (int i=0; i<16; i++) { // Fill the Top part of Region 1 if(i<4){ GameObject coin = GameObject.Instantiate(this.coinPrefab, new Vector3(Random.Range(-4.5f, 4.5f), 0.25f, Random.Range(3.0f, 4.5f)), this.coinPrefab.transform.rotation) as GameObject; coin.name = "R1TopC"+i; } // Fill the Bottom part of Region 1 if(i>3 && i<8){ GameObject coin = GameObject.Instantiate(this.coinPrefab, new Vector3(Random.Range(-4.5f, 4.5f), 0.25f, Random.Range(-4.5f, -3.0f)), this.coinPrefab.transform.rotation) as GameObject; coin.name = "R1BottomC"+i; } // Fill the Left part of Region 1 if(i>7 && i<12){ GameObject coin = GameObject.Instantiate(this.coinPrefab, new Vector3(Random.Range(-4.5f, -3.0f), 0.25f, Random.Range(-4.5f, 4.5f)), this.coinPrefab.transform.rotation) as GameObject; coin.name = "R1LeftC"+i; } if(i>11 & i<16){ GameObject coin = GameObject.Instantiate(this.coinPrefab, new Vector3(Random.Range(3.0f, 4.5f), 0.25f, Random.Range(-4.5f, 4.5f)), this.coinPrefab.transform.rotation) as GameObject; coin.name = "R1RightC"+i; } } // Coins for Region 2 for (int i=0; i<4; i++) { GameObject coin = GameObject.Instantiate(this.coinPrefab, new Vector3(Random.Range(-2.0f, 2.0f), 0.25f, Random.Range(1.5f, 2.0f)), this.coinPrefab.transform.rotation) as GameObject; coin.name = "R2C"+i; } // Coins for Region 3 for (int i=0; i<4; i++) { GameObject coin = GameObject.Instantiate(this.coinPrefab, new Vector3(Random.Range(-2.0f, 2.0f), 0.25f, Random.Range(-0.5f, 0.5f)), this.coinPrefab.transform.rotation) as GameObject; coin.name = "R3C"+i; } // Coins for Region 4 for (int i=0; i<4; i++) { GameObject coin = GameObject.Instantiate(this.coinPrefab, new Vector3(Random.Range(-2.0f, 2.0f), 0.25f, Random.Range(-2.0f, -1.5f)), this.coinPrefab.transform.rotation) as GameObject; coin.name = "R4C"+i; } #endregion ... } ... }
我们引入了一个名为coinPrefab的新变量,类型为GameObject,用于存储您猜对了,就是我们上一篇文章中已经定义过的金币预制件。该变量是公共的,因此您可以拖放预制件将其分配给脚本。
下一个重大变化是在我们的Start()函数中。我们为每个区域定义了一个循环。区域2、3和4的循环相当直接,但区域1的循环有点复杂。这是因为区域1本身技术上由4个虚拟区域组成:顶部、底部、左侧和右侧。
对于区域2、3和4,我们分别分配4个金币。对于区域1,我们分配16个金币,细分为4个虚拟区域:顶部、底部、左侧和右侧。
所以现在游戏中总共有12 + 16 = 28个金币,它们是随机放置在关卡中的。您现在可以从Hierarchy Window中删除您的设计时金币。删除设计时金币后,您的关卡应该看起来像这样:
图3-无设计时金币的关卡
当您运行游戏时,您的关卡将充满代码逻辑中定义的28个随机放置的金币。
图4-运行时动态生成的金币
所以到目前为止,我们已经解决了在运行时随机创建和放置金币的问题。一切都很好,但在此之前,您还有一些场景和改进需要完成,才能真正庆祝!
真正的重置
如果您已经走到了这一步,在继续前进之前,请继续运行游戏。玩几次游戏,并注意您的设计时环境。您注意到什么?
好吧,每次创建新会话时,前一个会话中的金币都会被带到新会话中。因此,您会得到更多的金币。这实际上不是我们想要的。当我们重置游戏,即开始新会话时,我们希望它从头开始!因此,我们需要在新会话开始之前进行一些清理。
为了处理这种情况,我们需要某种方式来检测场景中现有的金币对象,并在开始新游戏会话之前将其从场景中删除。为此,我们需要再次更新我们的脚本来处理这种情况。您可以在两个地方处理此问题:在Start()函数或butPlayAgain_Click()函数中。我将把处理这种情况的逻辑放在butPlayAgain_Click()函数中,并且列表将修改如下:
... public void butPlayAgain_Click(){ // get all object of type coin GameObject[] coins = GameObject.FindGameObjectsWithTag ("coin"); // remove eahc object from the scene foreach (GameObject coin in coins){ Destroy(coin); } Start (); } ...
此更新将确保我们在开始新会话之前始终从场景中删除之前创建的金币。
完整的列表将如下所示:
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; public GameObject coinPrefab; // variable used to store the coin prefab 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 () { #region COIN CREATION // we need to create the coins dynamically per region // Coins for Region 1 for (int i=0; i<16; i++) { // Fill the Top part of Region 1 if(i<4){ GameObject coin = GameObject.Instantiate(this.coinPrefab, new Vector3(Random.Range(-4.5f, 4.5f), 0.25f, Random.Range(3.0f, 4.5f)), this.coinPrefab.transform.rotation) as GameObject; coin.name = "R1TopC"+i; } // Fill the Bottom part of Region 1 if(i>3 && i<8){ GameObject coin = GameObject.Instantiate(this.coinPrefab, new Vector3(Random.Range(-4.5f, 4.5f), 0.25f, Random.Range(-4.5f, -3.0f)), this.coinPrefab.transform.rotation) as GameObject; coin.name = "R1BottomC"+i; } // Fill the Left part of Region 1 if(i>7 && i<12){ GameObject coin = GameObject.Instantiate(this.coinPrefab, new Vector3(Random.Range(-4.5f, -3.0f), 0.25f, Random.Range(-4.5f, 4.5f)), this.coinPrefab.transform.rotation) as GameObject; coin.name = "R1LeftC"+i; } if(i>11 & i<16){ GameObject coin = GameObject.Instantiate(this.coinPrefab, new Vector3(Random.Range(3.0f, 4.5f), 0.25f, Random.Range(-4.5f, 4.5f)), this.coinPrefab.transform.rotation) as GameObject; coin.name = "R1RightC"+i; } } // Coins for Region 2 for (int i=0; i<4; i++) { GameObject coin = GameObject.Instantiate(this.coinPrefab, new Vector3(Random.Range(-2.0f, 2.0f), 0.25f, Random.Range(1.5f, 2.0f)), this.coinPrefab.transform.rotation) as GameObject; coin.name = "R2C"+i; } // Coins for Region 3 for (int i=0; i<4; i++) { GameObject coin = GameObject.Instantiate(this.coinPrefab, new Vector3(Random.Range(-2.0f, 2.0f), 0.25f, Random.Range(-0.5f, 0.5f)), this.coinPrefab.transform.rotation) as GameObject; coin.name = "R3C"+i; } // Coins for Region 4 for (int i=0; i<4; i++) { GameObject coin = GameObject.Instantiate(this.coinPrefab, new Vector3(Random.Range(-2.0f, 2.0f), 0.25f, Random.Range(-2.0f, -1.5f)), this.coinPrefab.transform.rotation) as GameObject; coin.name = "R4C"+i; } #endregion this.score = 0; this.levelTime = Time.time + 30.00f; this.numOfCoinsCollected = 0; this.END_GAME = false; if (this.endGameCanvas != null) { 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{ if(this.endGameCanvas != null){ 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(){ // get all object of type coin GameObject[] coins = GameObject.FindGameObjectsWithTag ("coin"); // remove eahc object from the scene foreach (GameObject coin in coins){ Destroy(coin); } Start (); } }
每个人都喜欢金币!
既然我们已经解决了动态创建金币和重置游戏的问题,现在让我们看看如何稍微改进一下游戏的视觉效果。我首先想做的是让我的金币看起来和感觉像金币。
为了实现这一点,我需要创建一个具有代表金币纹理的材质。让我们看看如何实现这一点。
步骤
-
在项目中创建一个“Materials”文件夹,以进行组织和管理。
-
在该文件夹中,右键单击并从“上下文菜单”中选择Create->Material。将新材质对象命名为:gold_coin
-
分配代表金色表面的纹理,或选择一种颜色应用于材质。
-
将新材质分配给金币预制件!
注意:材质用于将纹理和/或颜色分配给GameObject。
在这种情况下,由于我们已经有了用于表示我们收集的金币的纹理,我们可以使用相同的纹理应用于我们的材质。如果应用相同的纹理,您将看到类似以下内容:
图5-创建的新材质
一旦定义了材质,您就需要选择prefabs文件夹下的coin预制件。然后将新材质拖放到Inspector Window中,以将材质分配给金币预制件。
注意:本文不涉及纹理等…所以纹理不会很好看!只是演示概念。
图6-带有新材质的金币
运行程序时,您现在会看到金币正在使用新创建的材质。由于我不是图形设计师,所以您可以看到金币看起来不像它应该的那样好。但尽管如此,您应该能明白这个概念。
如果您已经完成了到目前为止的所有步骤,您将获得类似以下的玩法体验:
关注点
我们的游戏正在缓慢成型。到目前为止,我们在定义和微调我们这个小小游戏方面已经走了很长一段路。随着系列的进展,您会发现我花在解释基础知识上的时间越来越少,花在新概念上的时间越来越多。
我们的游戏仍有很大的改进空间。在下一部分,我们将研究如何创建一个开始屏幕,如何在游戏中保存和加载数据,甚至可能通过引入更多挑战来改进我们的游戏玩法。
历史
这是我将缓慢为Code Project社区贡献的系列文章的第七篇。
- Unity 3D – 游戏编程 – 第 1 部分
-
Unity 3D – 游戏编程 – 第 7 部分
Unity 3D 网络文章
Unity 3D Leap Motion 和 Oculus Rift 文章