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






4.80/5 (9投票s)
这是本系列文章的第十篇,讨论Unity 3D以及如何开始您的3D项目。
引言
在本系列文章的第十部分,我们将增强我们的GameMaster.cs代码库,并为玩家提供一些游戏玩法选项。我还会给游戏中引入另一个元素。
如果您还没有阅读,请花点时间阅读
- Unity 3D – 游戏编程 – 第 1 部分
-
Unity 3D – 游戏编程 – 第 10 部分
Unity 3D 网络文章
Unity 3D Leap Motion 和 Oculus Rift 文章
在本系列的第一部分,我们从Unity 3D环境的最基础知识开始。了解IDE以及您将在整个项目中使用的不同部分。我们还介绍了如何使用设计器中的工具对选定的GameObject应用不同的变换:定位、旋转和缩放。最后,我们研究了如何创建第一个脚本,并使用该脚本在我们的立方体上应用Y轴旋转变换。
在本系列的第二部分中,我们更多地研究了通过编码对给定对象进行变换。我们还研究了如何创建对场景中对象的渲染至关重要的光源。
在本系列的第三部分中,我们研究了如何通过键盘处理用户输入,并根据键码采取特定操作。
在本系列的第四部分,我们研究了创建简单的用户界面。我们开发的界面为用户提供了反馈机制,也为用户提供了输入到我们的游戏或模拟中的另一种方法。
在第五部分,我们提出了一个简单游戏的构想。我们还研究了如何将 3D 模型导入游戏引擎。
在第六部分,我们扩展了我们的游戏构想,使其更有趣、更完整。我们引入了几个关于游戏玩法和游戏设计的概念,并为我们的游戏创建了一个简单的用户界面。
在第七部分,我们回答了第六部分提出的一些问题。
在第八部分,我们为玩家创造了更好的用户体验和视觉效果。
在本系列第九部分,我们增强了我们小游戏的视觉效果。我们还引入了游戏开始菜单,用户可以在其中选择一些选项,还可以确定游戏的难度。此外,我们还引入了声音效果和视觉效果,以增强游戏的整体外观和感觉。
在第十部分,我们将实现游戏选项功能,该功能允许用户控制我们已经在第9部分中实现的背景噪音音量和音效音量。此外,我们将引入另一个游戏玩法元素,一个生命值条对象,如果我们能及时捕获它,就可以增加我们的生命值!
Windows Phone 8.x 演示
我提供了一个免费的手机应用程序,您可以下载并在您的Windows Phone上预览演示。要下载移动应用程序,请访问以下链接: CodeProjectArticleSample
文章代码和视觉效果的实时预览
实时预览链接:http://www.noorcon.com/CodeProject/CodeProjectArticlePreview.html
背景
注意:对于本粒子,我将使用Expression Design创建一些简单的菜单和按钮或其他视觉组件,这些组件将用于增强游戏的视觉吸引力!
注意:对于本篇文章,我将使用 SketchUp 创建一些简单的积木,并将其导入 Unity!我不是 3D 建模师或设计师,所以请耐心等待并原谅我的不足!
假定本文的读者对一般编程概念有所了解。还假定读者对C#语言有理解和经验。还建议本文的读者熟悉面向对象编程和设计概念。我们将在文章中根据需要简要介绍它们,但不会深入细节,因为它们是完全独立的主题。我们还假定您对学习3D编程充满热情,并具备3D图形和向量数学的基本理论概念。
最后,本文使用Unity 3D版本4.6.1,这是初始发布日期时的最新公开发行版本。系列中讨论的大多数主题都将与旧版本的游戏引擎兼容,甚至可能与今年某个时候应该发布的最新版本兼容。但是,有一个主题在当前4.6.1版本与旧版本的游戏引擎相比有显著不同,那就是UI(用户界面)管道。这是因为引擎中新的UI架构比我们在此版本发布之前的UI架构要优越得多。我个人对新的UI架构非常满意。
使用代码
下载文章系列的项目/源代码: 下载源代码。
随着后续文章的提交,项目/源代码也将随之扩展。新的项目文件和源文件将包含系列中较旧的部分。
注意:要获取最新代码,请转到本系列最新发布的部分并下载代码。
实现游戏选项
您可能已经注意到,从第9部分开始,主菜单中有一个图标,通常用于应用程序中的设置/选项。在第10部分,我们将实现负责游戏中选项菜单的按钮。
图1 - 显示选项的主菜单截图
图1展示了我们游戏主界面的完整UI(用户界面)。为了实现所示界面,我不得不在我们现有的UI Canvas中添加一些UI元素。我将继续列出这些元素,但不会讨论基础知识。请参阅第4部分和第5部分了解概念。
图2 - 层级窗口显示Canvas层级
图2显示了在文章第9部分和第10部分中实现的GoldRushCanvas的层级结构。第10部分的主要新增项由数字标示。请注意命名方式,您会发现命名方式经过设计,可以帮助您了解使用了哪些UI组件以及它们是如何相互连接的。如果您注意到,有一个OptionsCanvas,这是代表整个选项UI元素的Canvas。我们拥有这个子Canvas的主要原因是我们可以通过代码启用和禁用它。其余的UI元素只是OptionsCanvas的子元素。我们有以下内容:
-
OptionsPanel
-
lblMusicControl
-
MusicSlider
-
lblSoundFx
-
FxSlider
OptionsPanel用于包含和组织子元素。lblMusicControl只是一个显示标题的文本UI对象,lblSoundFx也是如此。MusicSlider是一个滑块UI对象,将用于控制背景音乐的音量,FxSlider也是如此。
一如既往,您需要为两个滑块创建事件处理程序。所以请继续为两个滑块创建On Value Changed事件。如果您一直按照说明进行操作,并使用了相同的命名和结构,您可以继续将MainCamera GameObject分配到插槽中。
注意:在我的项目中,MainCamera GameObject包含处理所有菜单交互的MenuSelection.cs脚本。如果您操作方式不同,您的可能会有所不同。
现在,在您实际分配函数之前,我们需要创建处理背景音乐和音效音量的函数。
我们需要更新我们的MenuSelection.cs脚本
using UnityEngine; using UnityEngine.UI; using System.Collections; public class MenuSelection : MonoBehaviour { public Scrollbar level8progressbar; public Scrollbar level9progressbar; public Canvas optionsCanvas; public Slider gameMusicControl; public Slider gameFxControl; private bool DISPLAY_OPTIONS; // Use this for initialization void Start () { this.DISPLAY_OPTIONS = false; if (this.gameMusicControl != null) { this.gameMusicControl.value = GameMaster.gameMusicVolume; this.gameObject.audio.volume = GameMaster.gameMusicVolume; } if (this.gameFxControl != null) { this.gameFxControl.value = GameMaster.gameFxVolume; } if (this.optionsCanvas != null) this.optionsCanvas.enabled = this.DISPLAY_OPTIONS; } // Update is called once per frame void Update () { if (this.level8progressbar != null) { this.level8progressbar.size = Application.GetStreamProgressForLevel("part_8"); } if (this.level9progressbar != null) { this.level9progressbar.size = Application.GetStreamProgressForLevel("part_9"); } if (this.optionsCanvas != null) this.optionsCanvas.enabled = this.DISPLAY_OPTIONS; } public void ExitApplication(){ Application.Quit(); } public void DisplayOptions(){ this.DISPLAY_OPTIONS = !this.DISPLAY_OPTIONS; } public void GameSound(){ GameMaster.gameMusicVolume = this.gameMusicControl.value; this.gameObject.audio.volume = GameMaster.gameMusicVolume; } public void GameFxVolume(){ GameMaster.gameFxVolume = this.gameFxControl.value; } public void LoadMainMenu(){ Application.LoadLevel ("main_menu"); } public void LoadPart1(){ Application.LoadLevel ("part_1"); } public void LoadPart2(){ Application.LoadLevel ("part_2"); } public void LoadPart3(){ Application.LoadLevel ("part_3"); } public void LoadPart4(){ Application.LoadLevel ("part_4"); } public void LoadPart5(){ Application.LoadLevel ("part_5"); } public void LoadPart6(){ Application.LoadLevel ("part_6"); } public void LoadPart7(){ Application.LoadLevel ("part_7"); } public void LoadPart8(){ if (Application.CanStreamedLevelBeLoaded("part_8")) Application.LoadLevel("part_8"); } public void LoadPart9(){ if (Application.CanStreamedLevelBeLoaded ("part_9")) Application.LoadLevel ("part_9"); } }
我在上面的代码中引入了几个变量:optionsCanvas(一个Canvas对象),gameMusicControl和gameFxControl(都是Slider对象),最后是一个布尔类型的DISPLAY_OPTIONS。
Start()函数已更新,以确保optionsCanvas不显示,方法是将DISPLAY_OPTIONS变量设置为false。此变量控制Canvas的可见性。然后,我们检查gameMusicControl和gameFxControl是否已设置,如果已设置,则为它们分配所需的初始值。
注意:我们正在使用GameMaster类来获取和设置将在整个游戏中使用到的变量。
在Update()函数中,我们检查DISPLAY_OPTIONS是否设置为true,如果为true,则显示optionsCanvas。
DisplayOptions()函数设置DISPLAY_OPTIONS变量的值,它基本上每次按下UI中的按钮时都会反转该值。
GameSound()和GameFxVolume()函数用于设置背景音乐和音效的内部音量。再次注意,这是通过GameMaster对象完成的。
实现这些函数并保存脚本后,请继续将适当的函数分配给您通过IDE创建的事件。
GameMaster类
GameMaster.cs脚本用于保存将在不同时间和场景中跨游戏共享的一些通用数据。我们实现的GameMaster非常简单,并没有真正利用单例的概念。我们稍后会讲到。但就演示而言,其实现确实模拟了类似的东西。
GameMaster的目的是集中管理和共享游戏中的通用资源。这些包括游戏整体状态、玩家生命值、敌人、寻路点、声音管理器、库存、音频等各种事物,这取决于您的游戏。但简而言之,它是您游戏的粘合剂和大脑,使一切保持同步!
我们当前的GameMaster非常简单直接,让我们看一下代码。
using UnityEngine; using UnityEngine.UI; using System.Collections; public class GameMaster : MonoBehaviour { public static GameMaster GM; // static variable to hols instance of GameMaster public static int GameMode; // variable to define Game Mode public static bool EndGame; // variable to define End of Game public static float playerSpeed = 50.0f; // initial player speed public static float gameMusicVolume = 0.75f; // game background music level public static float gameFxVolume = 0.75f; // game sound fx level public GameObject playerHitFx; // visual fx for player hit public GameObject cannonBallFireFx; // visual fx for cannon ball blast off public GameObject playerHealthFx; // visual fx for health up public Image progressBar; // visual representation of level load public Canvas loadingPanelCanvas; // should be moved to MenuSelection.cs // Use this for initialization void Start () { GameMaster.EndGame = false; GameMaster.GM = this; DontDestroyOnLoad (this); if (this.progressBar != null) this.progressBar.fillAmount = 0.0f; } // Update is called once per frame void Update () { if (this.progressBar != null) { this.progressBar.fillAmount = Application.GetStreamProgressForLevel("part_9_game"); } if (Application.CanStreamedLevelBeLoaded ("part_9_game")) { if(this.loadingPanelCanvas!=null) this.loadingPanelCanvas.enabled = false; } } // this function should be moved to MenuSelection.cs public void butEasy(){ GameMaster.GameMode = 0; this.LoadGame (); } // this function should be moved to MenuSelection.cs public void butHard(){ GameMaster.GameMode = 1; this.LoadGame (); } // this function should be moved to MenuSelection.cs private void LoadGame(){ if (Application.CanStreamedLevelBeLoaded ("part_9_game")) { Application.LoadLevel ("part_9_game"); } } }
GameMaster.cs脚本已包含在之前的文章发布中,但我们从未讨论过。花点时间研究一下这个类。这里没有发生什么大事。正如您所见,我们正在定义将在整个游戏中使用的一些变量。
让我们看一下显而易见的变量:playerHitFx、cannonBallFireFx和playerHealthFx都是GameObject变量,将引用我们在游戏中使用的视觉效果。变量playerSpeed、gameMusicVolume和gameFxVolume都是浮点数变量,分别代表玩家基于其生命值的速度、游戏背景音乐的音量以及音效的音量。请注意,这些是静态变量。
下一个重要的变量是GM变量,其类型为GameMaster,并定义为静态。这是指向初始化后的GameMaster对象的变量。
回顾并访问MenuSelection.cs脚本,您会注意到我们正在访问GameMaster以获取声音音量的初始值,这些值设置为75%,由0.75f定义。然后,我们使用此值设置实际的背景音乐和音效音量。每次更改/调整滑块时,GameMaster都会跟踪音量级别。
此外,还定义了一些函数,butEasy()和butHard(),用于控制游戏模式。这些函数可能会移至MenuSelection.cs脚本。目前它们已在GameMaster中实现。
添加新的游戏玩法元素
通常,当您从事软件开发项目,或者实际上任何事物时,其设计和实现过程并非一定要仅仅是软件,您会获得更多更好的想法来改进它。嗯,我们这个小游戏演示也是如此。
有一些元素可以增强游戏玩法。一种是,如果玩家被炮弹击中,他/她将如何能够恢复生命值?另一种是,我们能否引入一个元素来增加时间?这两个问题的答案都是肯定的!
我已经实现了一个建议。玩家在被炮弹击中后能够恢复生命值是很好的。因此,我创建了一个新的GameObject来处理这个问题。同样,由于这不是一个图形竞赛,我复制了CannonBall Prefab并将其重命名为HealthBall。
但是,在我创建了HealthBall Prefab之后,我做了一些修改。首先要做的是将Tag属性更改为除cannon_ball以外的其他内容。该标签保留给CannonBall Prefab,并且是我们如何在脚本中识别它来处理PlayerInput.cs脚本中的OntriggerEnter()事件。所以我创建了一个名为health_ball的新标签来标识我们的HealthBall Prefab。
接下来,我删除了CannonBallSize.cs脚本,并创建了一个名为HealthBallSize.cs的新脚本并将其附加到HealthBall Prefab。脚本中的实际代码是相同的,但我们将其分开,因为将来我们可能会让它们执行不同的操作。
最后,我创建了一个名为health_ball的新材质,并将其颜色设为绿色。还将此材质附加到HealthBall Prefab上,替换现有材质。
HealthBallSize.cs脚本的列表
using UnityEngine; using System.Collections; public class HealthBallSize : MonoBehaviour { public int mass; // used to scale and also calculate damage public AudioClip healthPickUp; // clip to play after we pick-up health // Use this for initialization void Start () { this.gameObject.audio.volume = GameMaster.gameFxVolume; this.mass = Random.Range (1, 3); this.transform.localScale = new Vector3 (this.transform.localScale.x * this.mass, this.transform.localScale.y * this.mass, this.transform.localScale.z * this.mass); } // Update is called once per frame void Update () { } }
请注意,实例化预制件的缩放将基于1到3之间的随机数。此值将用于指示HealthBall为玩家提供多少生命值。巧合的是,CannonBallSize.cs脚本也使用了相同的逻辑,但情况恰恰相反。它将根据CannonBall的大小决定玩家将受到多少伤害。
另请注意,我们正在根据GameMaster.gameFxVolume变量调整音频音量。请记住,我们让每个CannonBall和HealthBall在Awake()时播放爆炸效果。这将确保我们的音效音量根据主菜单中用户选择的选项在场景之间传递。
我还创建了一个名为LaunchHealthBall.cs的新脚本。此脚本负责在游戏中实例化和管理HealthBalls。然后,该脚本被附加到Cannon Prefab上,因为Cannon Prefab是管理我们CannonBalls和HealthBalls发射的Prefab。
LaunchHealthBall.cs的列表
using UnityEngine; using System.Collections; public class LaunchHealthBall : MonoBehaviour { public float canonBallSpeed = 0.5f; // spped of our cannon ball public float rateOfFire = 5.5f; // cannon blast rate in seconds public float fireDelay; public GameObject healthBall; // used to store our health ball prefab public GameObject cannonBase; // used to store our cannon base public GameObject cannonStart; // used to store our cannon nozle starting point private GameObject fireObject; // Use this for initialization void Start () { this.canonBallSpeed = Random.Range (3, 4); // randomize cannon ball speed between 3 and 4 this.rateOfFire = Random.Range (6.5f, 13.0f); // randomize rate of fire between 3 and 6.5 seconds // set a fire delay time for the cannon this.fireDelay = Time.time + this.rateOfFire; } // Update is called once per frame void Update () { // if the current game running time is larger then our fire delay time, then fire! if (Time.time > this.fireDelay){ // cannon ball speed and rate of fire will be randomly generated based // on the defined boundaries ... this.canonBallSpeed = Random.Range (3, 4); this.rateOfFire = Random.Range (6.5f, 13.0f); // re-set our fire delay this.fireDelay = Time.time + this.rateOfFire; // instantiate our cannon ball at the specified location this.fireObject = GameObject.Instantiate(this.healthBall, this.cannonStart.transform.position, this.cannonStart.transform.rotation) as GameObject; // self-destruct our cannon ball after 10 seconds Destroy(this.fireObject, 10.0f); // use the physics engine to push/give velocity to our cannon ball this.fireObject.rigidbody.velocity = this.cannonStart.transform.TransformDirection( new Vector3(0,this.canonBallSpeed,0)); // used for the FX GameObject fx = GameObject.Instantiate(GameMaster.GM.cannonBallFireFx, this.cannonStart.transform.position, this.cannonStart.transform.rotation) as GameObject; Destroy(fx, 3.0f); } // rotate the cannon base around its Y-Axis continiously this.cannonBase.transform.Rotate (Vector3.up, 10 * Time.deltaTime); } }
此脚本中的所有内容都与LaunchCannon.cs类似,除了我们引用了HealthBall Prefab,并且我们的Random.Range()标准也不同,这会影响射速。
修改玩家输入
PlayerInput.cs脚本也已进行了一些修改和更新。我决定在玩家的生命值条上方显示实际的生命值。因此,有一个变量引用了Text UI对象,以便在需要时进行更新和显示值。但主要的变化是基于GameMaster对象来设置正确的音乐音量级别,以及玩家的速度等等...
最后,对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 Text lblNumOfCoins; // text UI element for displaying coins collected 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 health; // keep track of player health public Image healthBar; // visual health bar for player public Text healthBarCaption; // display actual health value over the health bar 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 () { GameMaster.EndGame = false; GameMaster.playerSpeed = 50.0f; this.gameObject.audio.volume = GameMaster.gameMusicVolume; #if UNITY_EDITOR Debug.Log (GameMaster.GameMode); #endif this.health = 100; if (this.healthBar != null) this.healthBar.fillAmount = 1.0f; if(this.healthBarCaption != null) this.healthBarCaption.text = this.health.ToString(); #region COIN CREATION // This checks to make sure we have the prefab, also so that it does not break previous code if(this.coinPrefab!=null){ // 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 + Random.Range (30.0f, 60.0f); 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); if (this.lblNumOfCoins != null) this.lblNumOfCoins.text = this.numOfCoinsCollected.ToString (); // 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 (!GameMaster.EndGame) { // 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){ GameMaster.EndGame = 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){ // used to detect if we collided with a coin GameObject 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(); if(this.lblNumOfCoins != null) this.lblNumOfCoins.text = this.numOfCoinsCollected.ToString(); // remove the Coin object from the scene Destroy(c.gameObject); } // used to detect if we collided with a cannon ball GameObject if (c.tag.Equals ("cannon_ball")) { // play the explosion sound fx when we collide c.gameObject.GetComponent<AudioSource>().Play(); #if UNITY_EDITOR string info = string.Format("{0}-{1}-{2}", c.name, "CANNON BALL!!!", c.gameObject.GetComponent<CannonBallSize>().mass); Debug.Log(info); #endif // reduce the health this.health -= 10 * c.gameObject.GetComponent<CannonBallSize>().mass; // also change the speed of the player ... when damaged it will go slower GameMaster.playerSpeed += c.gameObject.GetComponent<CannonBallSize>().mass*10; //GameMaster.playerSpeed * c.gameObject.transform.localScale.x; #if UNITY_EDITOR Debug.Log("SPEED:" + GameMaster.playerSpeed); #endif // remove the cannon ball object from the scene after a hit Destroy(c.gameObject); if(GameMaster.GM.playerHitFx != null) { GameObject fx = GameObject.Instantiate(GameMaster.GM.playerHitFx, this.transform.position, this.transform.rotation) as GameObject; if(this.healthBar != null){ this.healthBar.fillAmount = (this.health / 100.0f); } if(this.healthBarCaption != null){ this.healthBarCaption.text = this.health.ToString(); } Destroy(fx, 3.0f); } } if (c.tag.Equals ("health_ball")) { // c.gameObject.GetComponent<AudioSource>().PlayOneShot( // c.gameObject.GetComponent<HealthBallSize>().healthPickUp, // GameMaster.gameFxVolume); // c.gameObject.GetComponent<AudioSource>().SetScheduledEndTime(2.0f); if(this.health<100){ // reduce the health this.health += 10 * c.gameObject.GetComponent<HealthBallSize>().mass; // also change the speed of the player ... when damaged it will go slower GameMaster.playerSpeed -= c.gameObject.GetComponent<HealthBallSize>().mass*10; } if(this.health>100) this.health = 100; if(GameMaster.GM.playerHealthFx != null) { GameObject fx = GameObject.Instantiate(GameMaster.GM.playerHealthFx, this.transform.position, this.transform.rotation) as GameObject; if(this.healthBar != null){ this.healthBar.fillAmount = (this.health / 100.0f); } if(this.healthBarCaption != null){ this.healthBarCaption.text = this.health.ToString(); } Destroy(fx, 3.0f); } // remove the cannon ball object from the scene after a hit 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 ();*/ Application.LoadLevel (Application.loadedLevelName); } }
处理炮弹命中和生命球命中的主要工作包含在OnTriggerEnter()函数中。代码应该是自 explanatory的。您会注意到有一个新的条件,它会检查我们碰撞的对象是否被标记为health_ball,如果是,它将执行我们为获得生命值实现的逻辑。
图3 - 显示游戏玩法的添加内容
在图3中,您可以看到对游戏玩法的添加内容。(1)玩家生命值的数值表示,(2)生命球。
关注点
我们的小游戏正在慢慢成型,变得越来越有吸引力。需要考虑的一点是,您可能想实现另一个游戏玩法元素,让玩家能够增加或减少他们获取金币的时间。
此外,您可能想实现的另一个想法是创建特殊的隐藏传送门,将玩家从一个维度传送到另一个维度。这将是游戏玩法的一个很酷的小增强。新维度可能有一些怪物???好了,天空才是极限!您是自己世界的创造者和建筑师!
历史
这是我将逐步贡献给Code Project社区的系列文章的第十篇。
- Unity 3D – 游戏编程 – 第 1 部分
-
Unity 3D – 游戏编程 – 第 10 部分
Unity 3D 网络文章
Unity 3D Leap Motion 和 Oculus Rift 文章