游戏开发论坛

 找回密码
 立即注册
搜索
查看: 24585|回复: 0

教程:如何使用Unity制作3D版iOS游戏(2)

[复制链接]

1万

主题

1万

帖子

2万

积分

管理员

中级会员

Rank: 9Rank: 9Rank: 9

积分
20468
发表于 2013-2-23 15:47:56 | 显示全部楼层 |阅读模式
教程:如何使用Unity制作3D版iOS游戏(1)
教程:如何使用Unity制作3D版iOS游戏(2)

作者:Joshua Newnham

  这是使用Unity制作3D游戏教程系列的第2部分!(请点击此处查看第1部分

  在第1部分中,你已掌握与Unity相关的概念:

*Unity 3D界面
*资源
*材料与纹理
*场景定位
*照明
*相机定位
*物理学与碰撞器
*预制件

  现在,场景中的一切看起来相当粗糙,但到目前为止,这都出自Unity的视觉场景设计师之手。也就是说,你还未编写任何代码!

  因此在本篇教程中,你将学习利用代码为游戏注入生命,并为场景增加互动与动画的方法!

  本篇教程紧跟前文内容。如果你打算从“已知良好”状态的游戏入手,你可以使用我们在教程1中提到的项目。在Unity中打开这一内容,找到FileOpen Project,点击“Open Other”,浏览文件夹。记住场景不会默认加载,你需要打开,选择ScenesGameScene。

  现在开始本文内容。

  保证所有组件完美配合

  在深入探讨代码编写前,烦请您快速浏览下面图表,它显示出游戏中各个组件的功能与职责,以及它们之间的关系:

class-diagram-1from-raywenderlich.com%E5%89%AF%E6%9C%AC.jpg
class-diagram-1(from raywenderlich.com)

  位于中心位置的是GameController。它是个抽象的GameObject,意指说它与所有物理元素无关,而是用于场景操控。也就是说,其功能是协调游戏活动的各种状态,支持用户输入内容。

  接下来是ScoreBoard组件。这种封装方法主要用于更新场景中3D Text GameObjects的“分数”与“时间”。

  下一个是Player,其功能是对用户输入做出反应,管理球体的各种属性,包括其位置。

  最后是Ball。该对象的功能是触发特殊事件,表明“球”何时进入篮框,何时落到地面,标志着玩家的回合结束。

  脚本编写

  Unity引擎提供了几种不同脚本语言;其中包括Boo、Javascript(也就是UnityScript)与C#。总之,如果你曾有过web前端开发背景,那么UnityScript是最佳选择。

  然而,如果你更加熟悉C++、Java、Objective-C或C#语言,那么最好选择C#编写脚本任务。由于本网站大部分读者均具备Objective-C背景,因此,在本教程中,你将基于C#编写脚本。

  每个脚本对应一个Component,且附加到GameObject上。此外,你将延伸MonoBehaviour这个基础类,包括一系列预定义属性、方法与钩子。

  [注:想知道“钩子”定义?它是指传递到某些事件中所有Component的回调函数或消息,比如当两个碰撞器有交集时便会调用OnTriggerEnter方法。]

  我们做个试验!在“项目”面板中选择“脚本”文件夹,点击“创建”,再单击“C#脚本”:

Projectfrom-raywenderlich.png
Project(from raywenderlich)

  在检查器中,你将看到一份默认脚本,内容如下:
  1. using UnityEngine;
  2. using System.Collections;public class DummyScript : MonoBehaviour {// Use this for initialization
  3. void Start () {}// Update is called once per frame
  4. void Update () {}
  5. }
复制代码

  上面的Start()与Update()方法便是钩子方法;也称为“记号”,即在更新每一帧时被调用。游戏引擎的一个核心性能是不断更新与渲染循环。每当移动对象,场景便会重新渲染。对象再次移动,再次渲染。如此循环。

  首次实例化一个“组件”时会调用Awake()方法。一旦它用于所有活跃“组件”,随后便会调用Start()方法。接着是在更新每一帧或“记号”时会调用Update()方法。

  [注:MonoBehaviour中还有另一种更新方法为FixedUpdate()。它由物理引擎调用,仅在更新Rigidbody或其它物理属性时使用。之所以称其为FixedUpdate(),是因为它能保证固定间隔的调用,不像Update()方法在每次“记号”时调用,而记号间的时间并不固定。]

  记分板

  首先从编写ScoreBoard脚本入手,其实该脚本编写相当简单。你已经创建了一个脚本,因此只需重命名为“ScoreBoard”,双击打开。

  相信你还不知道Unity引擎中包括MonoDevelop!

ScoreBoardfrom-raywenderlich.png
ScoreBoard(from raywenderlich)

  [注:MonoDevelop是指针对C#语言开发,探讨本教程范围外所有性能与功能的完整集成开发环境。但如果你只局限于编辑与保存文件也无大碍。更多先进性能可在MonoDevelop中找到。]

  在新脚本中插入如下代码:
  1. using UnityEngine;
  2. using System.Collections;public class ScoreBoard : MonoBehaviour
  3. {public TextMesh pointsTextMesh;
  4. public TextMesh timeRemainingTextMesh;void Awake ()
  5. {
  6. }void Start ()
  7. {
  8. }void Update ()
  9. {
  10. }public void SetTime (string timeRemaining)
  11. {
  12. timeRemainingTextMesh.text = timeRemaining;
  13. }public void SetPoints (string points)
  14. {
  15. pointsTextMesh.text = points;
  16. }
  17. }
复制代码

  上面脚本介绍了公开访问属性的概念。此时的属性是指“记分板”子对象中3D Text的“分数”与“时间”。

  公开这些属性意味着它们在“检查器”面板中可视,那样你便可在设计时间内通过编辑器指定它们。一旦完成,你便可以调用SetTime()与SetPoints()的设置方法修改文本属性。

  完成上面的脚本创建后,你应切换回Unity,将它附加到“记分板”对象上。只要拖动脚本对象到“记分板”顶端便可完成。

  接着,将教程1中的各个3D Text子对象拖到右表中的相应位置:

Scripting-ScoreBoardfrom-raywenderlich.jpg
Scripting-ScoreBoard(from raywenderlich)

  这样便创建出“3DText”子对象与脚本属性的连接。很简单吧。

  测试

  在继续行动前,我们应确保一切如预期般运作。首先创建一个可更新记分板上时间与分数的新脚本。命名为“ScoreboardTest”,并复制如下代码:
  1. using UnityEngine;
  2. using System.Collections;public class ScoreBoardTest : MonoBehaviour
  3. {public ScoreBoard scoreboard;public void Start()
  4. {
  5. scoreboard.SetTime( “60″ );
  6. scoreboard.SetPoints( “100″ );
  7. }}
复制代码

  接着点击GameObjectCreate Empty,重命名为“ScoreboardTest”,并将此脚本附加到本游戏对象上(注:采用拖动方式)。接着将场景的记分板与ScoreBoardTest的记分板变量连接,点击开始。

unity3d-scoreboard-testfrom-raywenderlich.png
unity3d-scoreboard-test(from raywenderlich)

  哇,它运行了,你会在上面截图中看到记分板数字不断跳动!如果没有则需回顾之前步骤,查看可能失误。

  控制碰撞

  现在探讨Unity引擎控制物体碰撞的方式。

  之前说过,Ball的功能是在其通过篮框或落地时通知GameController。而且其上面附有Sphere Collider与Rigidbody,用于检测并对碰撞做出反应。在此脚本中,你可以通过倾听碰撞声正确通知GameController。

  如同之前做法,新建一个脚本“Ball”。而后在MonoDevelop环境中编辑如下代码:
  1. using UnityEngine;
  2. using System.Collections;[RequireComponent (typeof(SphereCollider))]
  3. [RequireComponent (typeof(Rigidbody))]
  4. public class Ball : MonoBehaviour
  5. {private Transform _transform;
  6. private Rigidbody _rigidbody;
  7. private SphereCollider _sphereCollider;public delegate void Net ();public Net OnNet = null;
  8. private GameController _gameController;void Awake ()
  9. {
  10. _transform = GetComponent<Transform>();
  11. _rigidbody = GetComponent<Rigidbody>();
  12. _sphereCollider = GetComponent<SphereCollider>();
  13. }void Start ()
  14. {
  15. _gameController = GameController.SharedInstance;
  16. }void Update ()
  17. {}public Transform BallTransform {
  18. get {
  19. return _transform;
  20. }
  21. }public Rigidbody BallRigidbody {
  22. get {
  23. return _rigidbody;
  24. }
  25. }public SphereCollider BallCollider {
  26. get {
  27. return _sphereCollider;
  28. }
  29. }public void OnCollisionEnter (Collision collision)
  30. {
  31. _gameController.OnBallCollisionEnter (collision);
  32. }public void OnTriggerEnter (Collider collider)
  33. {
  34. if (collider.transform.name.Equals (“LeftHoop_001″)) {
  35. if (OnNet != null) {
  36. OnNet ();
  37. }
  38. }
  39. }
  40. }
复制代码
  [注:由于该对象与GameController相互依存,因此有必要删除某些必要方法,之后实行空白标记法。此种情况下不能调用OnBallCollision()实例方法与ShareInstance()类方法。]

  以下是以代码块形式概述此脚本中引入的新概念:
  1. [RequireComponent (typeof (SphereCollider))]
  2. [RequireComponent (typeof (Rigidbody))]
复制代码
  Unity提供的类属性支持你在类中增加设计时间逻辑。此时,你应告知Unity引擎,此脚本依赖SphereCollider,而且RigidBody已附加到该脚本上。

  这是种良好习惯,尤其在项目规模不断扩大之际,它有助于自动为脚本中增添附加“组件”,避免出现不必要的漏洞。
  1. private Transform _transform;
  2. private Rigidbody _rigidbody;
  3. private SphereCollider _sphereCollider;void Awake ()
  4. {
  5. _transform = GetComponent<Transform>();
  6. _rigidbody = GetComponent<Rigidbody>();
  7. _sphereCollider = GetComponent<SphereCollider>();
  8. }
复制代码
  GetComponent()方法继承自MonoBehaviour的类,前者能针对某个特定组件类型搜索局部GameObject。没有则返回null,反之回到Component。由于这需要研究GameObject的所有组件,因此在频繁访问的情况下支持本地缓存(注:比如在调用Update()或FixedUpdate()方法时会有所需要)。
  1. private GameController _gameController;void Start () {
  2. _gameController = GameController.SharedInstance;
  3. }
复制代码

  由于此对象涉及GameController,因此可通知诸如碰撞这类事件。Game Controller将会是单例模式,可通过SharedInstance静态属性使用。
  1. public delegate void Net();
  2. public Net OnNet = null;
复制代码
  如果之前你从未使用过C#语言,那你便不大熟悉委托与事件。从根本上说,它们能够方便Component之间的交流。此时,外部Component会在OnNet事件中记录兴趣,借此随着Ball脚本催生出OnNet事件不断更新结果(实行观察者模式)。此概念极其类似iOS编程中采用的委托模式。

  1. public void OnCollisionEnter( Collision collision ){
  2. _gameController.OnBallCollisionEnter( collision );
  3. }
复制代码

  Ball的主要任务是在其通过篮网或落地时通知GameController。由于其上面附有Rigidbody,因此当它与地面的BoxCollider碰撞时,物理引擎会通过OnCollisionEnter()方法发送消息。

  此Collision参数会传递出更多相关细节,包括球体的碰撞对象。此时,只需将在GameController上输入此细节便可知晓解决方案。

  [注:除了OnCollisionEnter()方法,你还可调用OnCollisionStay()与OnCollisionExit()方法,即在对象与其它物体发生碰撞或停止碰撞的每一帧时调用。更多详情可登陆 http://docs.unity3d.com/Document ... ehaviour.html.Unity官方文档查询。]

  1. public void OnTriggerEnter( Collider collider ) {
  2. if( collider.transform.name.Equals ( “LeftHoop_001″ ) ) {
  3. if( OnNet != null ){
  4. OnNet();
  5. }
  6. }
  7. }
复制代码

  上述代码用于检测球体何时入网。还记得在上个教程中,你曾在篮网正下方设置了一个特殊的箱子碰撞器,并保存为触发器吗?

  由于这不属于技术“碰撞”,因此会出现OnTriggerEnter()这种单独回调函数(注:也可以是OnTriggerStay()与OnTriggerExit()),通常在与触发器碰撞时调用。

  此时,你需检查碰撞事件中的参与对象。如果它碰巧是篮网触发器,那可以通过OnNet方法通知GameController。

  相关做法如上所示!记住,在Unity中,你还不能将脚本附加到篮球对象上,因为此脚本依赖于你还未创建的GameController对象。

  上述内容着重球体方面,现在应考虑到Player!但在此之前我们应确保一切如预期般运作。

  测试

  首先,应为Ball脚本依附的GameController脚本创建一个存根,借此测试所有内容。因此新建一个脚本“GameController”,并替代如下内容:
  1. using UnityEngine;
  2. using System.Collections;public class GameController : MonoBehaviour {private static GameController _instance = null;public static GameController SharedInstance {
  3. get {
  4. if (_instance == null) {
  5. _instance = GameObject.FindObjectOfType (typeof(GameController)) as GameController;
  6. }return _instance;
  7. }
  8. }void Awake() {
  9. _instance = this;
  10. }public void OnBallCollisionEnter (Collision collision) {
  11. Debug.Log ( “Game Controller: Ball collision occurred!” );
  12. }}
复制代码

  这便是Singleton模式。它从属设计模式,能够保证系统中仅存在单个对象实例,类似全程变量或Highlander。

  因此,其它类便能较易访问GameController。作为一个连接完整的对象,它方便其它对象之间的互动,检查目前系统状态。

  [注:如果你十分好奇其它iOS项目中采用的Singleton模式,你可以在Stack Overflow中找到更多关于在iOS 4.1系统中实行单例模式的探讨内容。]

  为实现Singleton模式,调用静态方法可回到共享实例。如果还未设置共享实例,那可使用GameObjects的FindObjectOfType()静态法查找,这样可在场景中获得首个活动对象。

  [注:在执行Singleton模式时,通常设置构造函数为隐藏模式,因此其存取器可控制实例。由于是继承自Unity MonoBehaviour的类,因此我们无法将构造函数设置为隐藏模式。所以这是个隐含Singleton模式,程序员必须明确执行。]

  接着增加一个测试脚本,测试球体的所有碰撞行为,类似之前的记分板测试。

  为此,新建一个“BallTest”脚本,复制如下代码:
  1. using UnityEngine;public class BallTest : MonoBehaviour {
  2. public Ball ball;
  3. void Start () {
  4. ball.OnNet += Handle_OnNet;
  5. }protected void Handle_OnNet(){
  6. Debug.Log ( “NOTHING BUT NET!!!” );
  7. }
  8. }
复制代码

  而后采取如下方式进行测试:

*将“Ball”脚本拖到篮球对象顶端。
*新建一个空白游戏对象“GameController”,在此区域中复制GameController脚本。
*新建一个空白游戏对象“BallTest”,在此区域中复制BallTest脚本。
*点击BallTest对象,将Ball变量改为篮球。

  最后,将篮球对象定位在篮框上方,如下图所示:

unity3d-ball-test-1from-raywenderlich.png
unity3d-ball-test-1(from raywenderlich)

  点击开始,你会看到主机上显示“NOTHING BUT NET!!!”,并伴随一些调试消息!

debug-messagesfrom-raywenderlich.jpg
debug messages(from raywenderlich)

  此时,你已测试出球体脚本能够正确发觉常见碰撞或触发器碰撞,并可推动这些事件分别在OnNet处理器与GameController上进行。

  现在已清楚碰撞事件可正当运作,接着可以设置运动员!

  运动员框架

  现在,你只需执行Player代码存根。在完成GameController设置后可回到此阶段。

  新建一个“Player”脚本,在MonoDevelop环境中编辑如下代码:
  1. [RequireComponent (typeof(Animation))]
  2. public class Player : MonoBehaviour
  3. {public delegate void PlayerAnimationFinished (string animation);public PlayerAnimationFinished OnPlayerAnimationFinished = null;
  4. private Vector3 _shotPosition = Vector3.zero;
  5. public Vector3 ShotPosition{
  6. get{
  7. return _shotPosition;
  8. }
  9. set{
  10. _shotPosition = value;
  11. }
  12. }public enum PlayerStateEnum
  13. {
  14. Idle,
  15. BouncingBall,
  16. PreparingToThrow,
  17. Throwing,
  18. Score,
  19. Miss,
  20. Walking
  21. }private PlayerStateEnum _state = PlayerStateEnum.Idle;
  22. private float _elapsedStateTime = 0.0f;
  23. private Transform _transform;
  24. private Animation _animation;
  25. private CapsuleCollider _collider;
  26. private bool _holdingBall = false;void Awake ()
  27. {
  28. _transform = GetComponent<Transform>();
  29. _animation = GetComponent<Animation>();
  30. _collider = GetComponent<CapsuleCollider>();
  31. }void Start ()
  32. {
  33. }void Update ()
  34. {
  35. }public bool IsHoldingBall {
  36. get {
  37. return _holdingBall;
  38. }
  39. }public PlayerStateEnum State {
  40. get {
  41. return _state;
  42. }
  43. set {
  44. _state = value;
  45. _elapsedStateTime = 0.0f;
  46. }}public float ElapsedStateTime {
  47. get {
  48. return _elapsedStateTime;
  49. }
  50. }}
复制代码

  GameController主要依据了解何时完成动画,知晓并设置Player的目前状态。在处理动画事件上可调用OnPlayerAnimationFinished()事件。

  同时还可采用记录Player各种可能状态的枚举器:包括闲暇、运球、预备投篮、投篮、得分、失分、走动,各个状态均有对应属性。

  注意,基于C#语言的属性创建通常如下:
  1. public float MyProperty{
  2. get {
  3. return MyPropertyValue;
  4. }
  5. set{
  6. MyPropertyValue = value;
  7. }
  8. }
复制代码

  借此便能清晰便捷地控制“获得者”与“设置者”。

  记住,拖动“等级系统”面板上玩家对象顶端的Player脚本。

  这样便创建了Player,实现这个存根相当简单,我们命名该运动员为“Stubby”。接着是设置GameController!

  游戏控制器

  GameController的功能是协调游戏中的活动,接受用户输入。

  那么“协调活动”意味着什么?通常游戏的运作与状态机器一样。其当前状态能够决定运行哪部分代码,如何中断用户输入,以及屏幕前后的情况。

  在复杂游戏中,你常常会把各个状态封装到一个实体上,但在简单游戏中最好采用枚举法与语句切换控制各种游戏状态。

  这时便为GameController创建一个启动器脚本,现在我们开始内部构造。

  变量

  首先应标明所需变量。由于GameController主要用于协调所有物体,因此你需要参考大部分用于控制游戏统计数据的变量(比如当前得分、剩余时间等)。

  添加如下代码,标明变量(相关注释已插入到代码片段中):
  1. public Player player; // Reference to your player on the scene
  2. public ScoreBoard scoreBoard; // Reference to your games scoreboard
  3. public Ball basketBall; // reference to the courts one and only basketball
  4. public float gameSessionTime = 180.0f;  // time for a single game session (in seconds)
  5. public float throwRadius = 5.0f; // radius the player will be positioned for each throw
  6. private GameStateEnum _state = GameStateEnum.Undefined;    // state of the current game – controls how user interactions are interrupted and what is activivated and disabled
  7. private int _gamePoints = 0; // Points accumulated by the user for this game session
  8. private float _timeRemaining = 0.0f; // The time remaining for current game session
  9. // we only want to update the count down every second; so we’ll accumulate the time in this variable
  10. // and update the remaining time after each second
  11. private float _timeUpdateElapsedTime = 0.0f;
  12. // The original player position – each throw position will be offset based on this and a random value
  13. // between-throwRadius and throwRadius
  14. private Vector3 _orgPlayerPosition;
复制代码

  公开gameSessionTime(运动员的游戏时间)与throwRadius(篮球运动员可能需移动的距离)意味着在测试阶段方便调整。

  游戏状态

  你已为Player状态增加了一些状态;现在可以为游戏添加状态:
  1. public enum GameStateEnum
  2. {
  3. Undefined,
  4. Menu,
  5. Paused,
  6. Play,
  7. GameOver
  8. }
复制代码

  以下是关于游戏多种状态的解释:

*菜单——展示主菜单项
*暂停——类似主菜单
*开始——用户真正开始游戏进程
*结束——完成游戏

  状态如同关口,它会根据当前状态阻止某些路径(依据代码分支)。状态逻辑则贯穿在该类的所有方法中,但设置其为公开属性可用于控制状态切换。

  接着为游戏状态添加获取者与设置者,如下所示:
  1. public GameStateEnum State {
  2. get{
  3. return _state;
  4. }
  5. set{
  6. _state = value;
  7. // MENU
  8. if( _state == GameStateEnum.Menu ){
  9. Debug.Log( “State change – Menu” );
  10. player.State = Player.PlayerStateEnum.BouncingBall;
  11. // TODO: replace play state with menu (next tutorial)
  12. StartNewGame();
  13. }
  14. // PAUSED
  15. else if( _state == GameStateEnum.Paused ){
  16. Debug.Log( “State change – Paused” );
  17. // TODO; add pause state (next tutorial)
  18. }
  19. // PLAY
  20. else if( _state == GameStateEnum.Play ){
  21. Debug.Log( “State change – Play” );
  22. }
  23. // GAME OVER
  24. else if( _state == GameStateEnum.GameOver ){
  25. Debug.Log( “State change – GameOver” );
  26. // TODO; return user back to the menu (next tutorial)
  27. StartNewGame();
  28. }
  29. }
  30. }
复制代码

  将此状态封装在一个属性内,那样你便能轻易拦截状态更改,必要时执行必要逻辑(如上图所示)。

  支持方法与属性

  接着可以增加一些支持方法与属性。

  首先添加如下所示的StartNewGame方法:
  1. public void StartNewGame(){
  2. GamePoints = 0;
  3. TimeRemaining = gameSessionTime;
  4. player.State = Player.PlayerStateEnum.BouncingBall;
  5. State = GameStateEnum.Play;
  6. }
复制代码

  该方法主要用于重新设置游戏统计数据(上面标明的变量),为新游戏场景准备实体。

  接着添加ResumeGame方法:
  1. public void ResumeGame(){
  2. if( _timeRemaining < 0 ){
  3. StartNewGame();
  4. } else{
  5. State = GameStateEnum.Play;
  6. }
  7. }
复制代码

  它类似StartNewGame方法,但具备额外检查功能。在思考整个游戏方案后可调用此方法。否则GameController状态会切换回Play状态,即重新开始。

  接下来,为GamePoints确定一个新属性:
  1. public int GamePoints{
  2. get{
  3. return _gamePoints;
  4. }
  5. set{
  6. _gamePoints = value;
  7. scoreBoard.SetPoints( _gamePoints.ToString() );
  8. }
  9. }
复制代码

  它主要用于更新记分板分数。

  最后,添加一个TimeRemaining属性:
  1. public float TimeRemaining {
  2. get{
  3. return _timeRemaining;
  4. }
  5. set{
  6. _timeRemaining = value;
  7. scoreBoard.SetTime( _timeRemaining.ToString(“00:00″) );
  8. // reset the elapsed time
  9. _timeUpdateElapsedTime = 0.0f;
  10. }
  11. }
复制代码

  保证记分板能根据当前剩余时间更新分数。

  实行支持方法与属性后,时间会处在最新状态!

  保证一切处于最新状态

  现在应着眼于如何让GameController记录相应情况,此时可调用Update方法与追踪方式。在该组件上添加如下代码:
  1. void Update () {
  2. if( _state == GameStateEnum.Undefined ){
  3. // if no state is set then we will switch to the menu state
  4. State = GameStateEnum.Menu;
  5. }
  6. else if( _state == GameStateEnum.Play ){
  7. UpdateStatePlay();
  8. }
  9. else if( _state == GameStateEnum.GameOver ){
  10. UpdateStateGameOver();
  11. }
  12. }
  13. private void UpdateStatePlay(){
  14. _timeRemaining -= Time.deltaTime;
  15. // accumulate elapsed time
  16. _timeUpdateElapsedTime += Time.deltaTime;
  17. // has a second past?
  18. if( _timeUpdateElapsedTime >= 1.0f ){
  19. TimeRemaining = _timeRemaining;
  20. }
  21. // after n seconds of the player being in the miss or score state reset the position and session
  22. if( (player.State == Player.PlayerStateEnum.Miss || player.State == Player.PlayerStateEnum.Score)
  23. && player.ElapsedStateTime >= 3.0f ){
  24. // check if the game is over
  25. if( _timeRemaining <= 0.0f ){
  26. State = GameStateEnum.GameOver;
  27. } else{
  28. // set a new throw position
  29. Vector3 playersNextThrowPosition = _orgPlayerPosition;
  30. // offset x
  31. playersNextThrowPosition.x +=  Random.Range(-throwRadius, throwRadius);
  32. player.ShotPosition = playersNextThrowPosition;
  33. }
  34. }
  35. }
  36. private void UpdateStateGameOver(){
  37. // TODO; to implement (next tutorial)
  38. }
复制代码

  Update方法是根据当前状态把任务委托到特定方法上。正如你所看到的,UpdateStatePlay方法的代码片段上涉及的代码,详情如下。
  1. _timeRemaining -= Time.deltaTime;
  2. // accumulate elapsed time
  3. _timeUpdateElapsedTime += Time.deltaTime;
  4. // has a second past?
  5. if( _timeUpdateElapsedTime >= 1.0f ){
  6. TimeRemaining = _timeRemaining;
  7. }
复制代码

  第一部分用于更新游戏运作时间(或剩余时间)。使用 _timeUpdateElapsedTime变量追踪TimeRemaining属性的最后一次更新,将此更新速度降为以秒为单位,因为快速更新记分板(通过TimeReamining属性实现)没多大必要,可能会影响游戏性能。
  1. // after n seconds of the player being in the miss or score state reset the position and session
  2. if( (player.State == Player.PlayerStateEnum.Miss || player.State == Player.PlayerStateEnum.Score)
  3. && player.ElapsedStateTime >= 3.0f ){
  4. // check if the game is over
  5. if( _timeRemaining <= 0.0f ){
  6. State = GameStateEnum.GameOver;
  7. } else{
  8. // set a new throw position
  9. Vector3 playersNextThrowPosition = _orgPlayerPosition;
  10. // offset x
  11. playersNextThrowPosition.x +=  Random.Range(-throwRadius, throwRadius);
  12. player.ShotPosition = playersNextThrowPosition;
  13. }
  14. }
复制代码

  第二片段则用于检查篮球运动员何时完成投篮动作,游戏是否结束。如果他在3秒左右的时间内一直处在Miss或Score状态,那便意味着完成一次投篮。之所以有所延误,是因为在开始下一个投篮前,你希望有个动画来圆满此事件。

  接着应检查是否有时间剩余。如果没有,可将状态调整为GameOver,否则应指使篮球运动员移动到一个新位置,开始另一次投篮。

  测试

  之前,你已在“等级系统”中创建了一个GameController对象,并附加相应脚本,可见相应工作可告一个段落。

  在“等级系统”面板中选择GameController对象,你会发现其中某些运动员、记分板与篮球的属性为公开模式。通过拖动在“检查器”中设置它们为适当对象。

Inspectorfrom-raywenderlich1.png
Inspector(from raywenderlich)

  如今,当你点击开始按钮时,你会发现时间会以秒单位更新。

unity3d-game-controller-test-1from-raywenderlich.png
unity3d-game-controller-test-1(from raywenderlich)

  处理用户输入

  很多时候,你会发现自己基于台式机开发游戏,而后在项目接近尾声时,常常会被把它移植到实际设备上。结果,你需要处理这两种输入模式:一是触屏模式,二是键盘与鼠标。

  为此,首先应在GameController上添加辅助方法,检测该应用是否可在移动设备上运行:
  1. public bool IsMobile{
  2. get{
  3. return (Application.platform == RuntimePlatform.IPhonePlayer || Application.platform == RuntimePlatform.Android);
  4. }
  5. }
复制代码

  幸好该设计无需过多互动;所有必要条件都可决定手指是否在落在屏幕上,以下代码片段便具有此作用。
  1. public int TouchCount {
  2. get{
  3. if( IsMobile ){
  4. return Input.touchCount;
  5. } else{
  6. // if its not consdered to be mobile then query the left mouse button, returning 1 if down or 0 if not
  7. if( Input.GetMouseButton(0) ){
  8. return 1;
  9. } else{
  10. return 0;
  11. }
  12. }
  13. }
  14. }
  15. public int TouchDownCount {
  16. get{
  17. if( IsMobile ){
  18. int currentTouchDownCount = 0;
  19. foreach( Touch touch in Input.touches ){
  20. if( touch.phase == TouchPhase.Began ){
  21. currentTouchDownCount++;
  22. }
  23. }
  24. return currentTouchDownCount;
  25. } else{
  26. if( Input.GetMouseButtonDown(0) ){
  27. return 1;
  28. } else{
  29. return 0;
  30. }
  31. }
  32. }
  33. }
复制代码

  为了确定用户是否接触屏幕,你可以根据该应用的运作平台,分别使用TouchCount与TouchDownCount属性。

  如果是在移动平台上运行,通过查询(返回到)“输入”类型,检测触屏数量即可,否则便可断定该作在台式机上运作,查询MouseButton的输入量(点击结果为1,否则为0)。

  TouchCount与TouchDownCount两者的唯一区别是,前者计算手指在屏幕上的滑动次数,并没有考虑其发生阶段,而后者只计算开始阶段中的滑动次数。

[注:这种Touch类有个枚举法名为TouchPhase,一个触摸阶段基本上等同于当前触摸状态,比如,首次发觉(即手指首次触摸屏幕)触屏则制定为Began阶段,一旦滑动便是Moved阶段,拿开手指则为Ended阶段。]

  若想充分了解Unity的Input类,可参照Unity官方网站(http://docs.unity3d.com/Documentation/ScriptReference/Input.html).

  控制球体发送的消息

  回想起来,Ball会在两种情况下向GameController发送消息:一是它进入篮框,二是击中地面。

  调用OnBallCollisionEnter方法处理篮球碰撞地面的情况:
  1. public void OnBallCollisionEnter (Collision collision)
  2. {
  3. if (!player.IsHoldingBall) {
  4. if ((collision.transform.name == “Ground” ||
  5. collision.transform.name == “Court”) &&
  6. player.State == Player.PlayerStateEnum.Throwing) {
  7. player.State = Player.PlayerStateEnum.Miss;
  8. }
  9. }
  10. }
复制代码

  OnBallCollisionEnter()函数能够检测运动员是否抓住球。如果没有,那便断定球已投出。因此,如果球碰到地面或出界,那便表示此回合结束。如果它碰到地面或球场,且未投中篮框,那便可设置运动员的状态为Miss。

  Ball Component与HandleBasketBallOnNet事件会调用此函数。那该如何连接两者?可以在OnNet事件中记录‘兴趣’,并调用Start()方法。

  同时还可以将它们添加到新方法Start()上,此处十分适合放置初始化代码:
  1. void Start () {
  2. // register the event delegates
  3. basketBall.OnNet += HandleBasketBallOnNet;
  4. }
复制代码

  这便是为事件委托指定回调函数的方法。在Ball催生出Net事件时可调用HandleBasketBallOnNet方法。

  其实现方法如下:
  1. public void HandleBasketBallOnNet(){
  2. GamePoints += 3;
  3. player.State = Player.PlayerStateEnum.Score;
  4. }
复制代码

  控制来自运动员组件的消息

  另一与GameController互动的组件是Player。此时并没有将其考虑在内,但在这节中你将处理GameController上的消息与事件。Player会在动画结束后提出一个事件,反过来会触发GameController上游戏动态的更新。

  在Start()方法末尾添加如下代码,用于记录事件:
  1. player.OnPlayerAnimationFinished += HandlePlayerOnPlayerAnimationFinished;
复制代码

  以及附加方法:
  1. public void HandlePlayerOnPlayerAnimationFinished (string animationName)
  2. {
  3. if (player.State == Player.PlayerStateEnum.Walking) {
  4. player.State = Player.PlayerStateEnum.BouncingBall;
  5. }
  6. }
复制代码

  在运动员完成走路动作后,此代码会将其状态更改为BouncingBall。

  接下来,教程会将所有事件结合起来,保证你最终会投中几个篮框!

  运动员的必要功能:

  以下快速回顾了运动员的职责与必要功能:

*在闲暇时间,运动员可以运球。
*在Play状态时,运动员应对用户输入做出反应;此时,如若用户手指紧紧放在屏幕上,运动员便会‘汇聚力量’准备投篮。
*运动员会影响到篮球位置与其性能。
*运动员应在每回合结束后绕球场移动。
*运动员应根据当前状态有所表现,比如在球进入篮框后做出胜利动作,在错失时做出失望动作。
*在完成上述动作后,运动员应通知GameController。

  接着回过头来打开Player脚本,仔细浏览里面代码。

  角色动画

  Unity提供了一系列丰富类能用于处理3D动画包的导入与使用。在导入Blender中创建的运动员时会附加一系列动画。选择编辑器中PlayerObject的Animation Component便会看到如下情况:

player-animationsfrom-raywenderlich.png
player-animations(from raywenderlich)

  其中有10个时段,每个时段又包含一个Animation Clip。点击任意一个Animation Clip,便可播放脚本中的任何动画。

  [注:有关Animation Component的更多内容可以查看Unity官方文档:http://docs.unity3d.com/Document ... lass-Animation.html]

  在Player脚本中,添加一些变量可控制当前动画,AnimationClips主要参考如下动画:
  1. private AnimationClip _currentAnimation = null;
  2. public AnimationClip animIdle;
  3. public AnimationClip animBounceDown;
  4. public AnimationClip animBounceUp;
  5. public AnimationClip animWalkForward;
  6. public AnimationClip animWalkBackward;
  7. public AnimationClip animPrepareThrow;
  8. public AnimationClip animThrow;
  9. public AnimationClip animScore;
  10. public AnimationClip animMiss;
复制代码

  通过变量引用动画能够灵活便捷地更新动画,无需依赖特定动画文件或索引/名称。

  当然,为实现该理念,你不得不对应配合动画与脚本组件中的各个公开属性,那就马上行动吧:

Inspector-2from-raywenderlich1.png
Inspector(from raywenderlich)

  接下来是创建各个动画,此时可调用Player Start方法(同时参照附加动画组件)。添加如下代码:
  1. void Start(){
  2. _animation = GetComponent();
  3. InitAnimations();
  4. }
  5. private void InitAnimations ()
  6. {
  7. _animation.Stop ();
  8. _animation [animIdle.name].wrapMode = WrapMode.Once;
  9. _animation [animBounceDown.name].wrapMode = WrapMode.Once;
  10. _animation [animBounceUp.name].wrapMode = WrapMode.Once;
  11. _animation [animWalkForward.name].wrapMode = WrapMode.Loop;
  12. _animation [animWalkBackward.name].wrapMode = WrapMode.Loop;
  13. _animation [animPrepareThrow.name].wrapMode = WrapMode.Once;
  14. _animation [animThrow.name].wrapMode = WrapMode.Once;
  15. _animation [animScore.name].wrapMode = WrapMode.Once;
  16. _animation [animMiss.name].wrapMode = WrapMode.Once;
  17. _animation [animBounceDown.name].speed = 2.0f;
  18. _animation [animBounceUp.name].speed = 2.0f;
  19. }
复制代码

  Animation Component实则是动画的控制器与贮存器。每个动画都包含在AnimationState类中。你可以通过索引定位或关键字进行使用,此处的关键字是指动画名称。你可以在上面的编辑器截屏中看到。

  比如动画中的两个属性:即wrapMode与速度。后者决定特定动画的重播速度,而前者决定动画的‘包装’模式;也就是说,每个回合结束后的动画场面。此处的动画要么只上演一次,要么会循环反复。

  接下来只剩下播放动画!在Player类中添加如下代码:
  1. public bool IsAnimating{
  2. get{
  3. return _animation.isPlaying;
  4. }
  5. }
  6. public AnimationClip CurrentAnimation {
  7. get {
  8. return _currentAnimation;
  9. }
  10. set {
  11. SetCurrentAnimation (value);
  12. }
  13. }
  14. public void SetCurrentAnimation (AnimationClip animationClip)
  15. {
  16. _currentAnimation = animationClip;
  17. _animation [_currentAnimation.name].time = 0.0f;
  18. _animation.CrossFade (_currentAnimation.name, 0.1f);
  19. if (_currentAnimation.wrapMode != WrapMode.Loop) {
  20. Invoke (“OnAnimationFinished”, _animation [_currentAnimation.name].length /
  21. _animation [_currentAnimation.name].speed);
  22. }
  23. }
  24. private void OnAnimationFinished ()
  25. {
  26. if (OnPlayerAnimationFinished != null) {
  27. OnPlayerAnimationFinished (_currentAnimation.name);
  28. }
  29. }
复制代码

  上述代码展示出有关动画控制的所有方法。主要是SetCurrentAnimation()方法。

  此时,重设当前动画时间为O(即回到开始),那么Animation Component需要交叉渐变出特定动画。交叉渐变会随着当前动画呈现而消失。也就是说,当前动画会逐渐‘散开’,平缓过渡到新动画上。

  此后,应检查动画是否会循环反复。如果没有,便可采用Invoke方法推迟调用OnAnimationFinished()函数。而这要推迟到动画结束。

  最后,OnAnimationFinied()函数的功能是催生出连锁事件,从而通知GameController动画已经完成,从而知晓Player GameObject的当前状态与动作。

  测试

  我们应保证所有动画的设置与运行没有差池。为此,在Player启动方法末端添加下面代码:

  接着,不选择GameObject组件,禁用GameController脚本:

CurrentAnimation = animPrepareThrow;

  点击开始按钮:如果一切运作顺畅,那你便会看到篮球运动员做出“预备投篮”动作!

unity3d-disable-the-gamecontrollerfrom-raywenderlich1.png
unity3d-disable-the-gamecontroller(from raywenderlich)

  [注:在重新启动GameController脚本前,应删除测试代码片段。]

  控制状态

  现在应充实State属性(之前已创建完毕);但在此之前,我们应剔除以下必要方法。

  我们会在探讨篮球运动员如何运球方面详细解释此方法。但现在应将之前的State属性替换成以下代码片段:
  1. private void AttachAndHoldBall(){
  2. }
复制代码

  大部分代码都是有关基于当前设置状态,调用SetCurrentAnimation方法,设置适当动画。我们应注意某些重点代码:
  1. public PlayerStateEnum State{
  2. get{
  3. return _state;
  4. }
  5. set{
  6. CancelInvoke(“OnAnimationFinished”);
  7. _state = value;
  8. _elapsedStateTime = 0.0f;
  9. switch( _state ){
  10. case PlayerStateEnum.Idle:
  11. SetCurrentAnimation( animIdle );
  12. break;
  13. case PlayerStateEnum.BouncingBall:
  14. _collider.enabled = false;
  15. AttachAndHoldBall();
  16. SetCurrentAnimation( animBounceUp );
  17. break;
  18. case PlayerStateEnum.PreparingToThrow:
  19. SetCurrentAnimation( animPrepareThrow );
  20. break;
  21. case PlayerStateEnum.Throwing:
  22. SetCurrentAnimation( animThrow );
  23. break;
  24. case PlayerStateEnum.Score:
  25. SetCurrentAnimation( animScore );
  26. break;
  27. case PlayerStateEnum.Miss:
  28. SetCurrentAnimation( animMiss );
  29. break;
  30. case PlayerStateEnum.Walking:
  31. if( _shotPosition.x < _transform.position.x ){
  32. SetCurrentAnimation( animWalkForward );
  33. } else{
  34. SetCurrentAnimation( animWalkBackward );
  35. }
  36. break;
  37. }
  38. }
  39. }
复制代码

  比如首个语句:
  1. CancelInvoke(“OnAnimationFinished”);
复制代码

  该语句要求Unity取消排队调用OnAnimationFinished方法,你可能十分熟悉此方法,因为在上演非循环动画时曾用过。

  接下来的有趣代码片段是PlayerStateEnum.Walking;在此组块中,你会基于目标位置决定相应动画,而不是基于当前位置决定篮球运动员是否前进或后退。

  测试

  类似上面做法,快速检测角色状态与动作是否完美匹配。在Player类的Start方法中添加如下代码:
  1. State = PlayerStateEnum.Score;
复制代码

  如之前那般,不选择GameObject组件,禁用GameController脚本,以免干扰测试。

  点击开始按钮;如果一切运作顺畅,那你将会看到篮球运动员做出“得分”动作(在投篮成功时做出的动作)。

  [注:在重启GameController脚本前,应删除测试代码片段。]

  运球

  篮球运动员在等待用户输入期间的一个职责是运球。本部分中我们将揭示实现这种动作所需的代码与设置。

  首先,在Player类顶端标明变量:
  1. public Ball basketBall;
  2. public float bounceForce = 1000f;
  3. private Transform _handTransform;
复制代码

  变量_handTransform主要参照Transform组件的接触球,而bounceForce则用于决定运球所需的力量(篮球变量应相当明显)。


  问题是,当Player状态改为BouncingBall时,我们该如何定位此球在玩家手中的位置。此时可调用之前剔除的AttachAndHoldBall方法:
  1. public void AttachAndHoldBall ()
  2. {
  3. _holdingBall = true;
  4. Transform bTransform = basketBall.BallTransform;
  5. SphereCollider bCollider = basketBall.BallCollider;
  6. Rigidbody bRB = basketBall.BallRigidbody;
  7. bRB.velocity = Vector3.zero;
  8. bTransform.rotation = Quaternion.identity;
  9. Vector3 bPos = bTransform.position;
  10. bPos = _handTransform.position;
  11. bPos.y -= bCollider.radius;
  12. bTransform.position = bPos;
  13. }
复制代码

  其中一个公开变量与篮球对象有关。其功能需参照球体转变、碰撞与刚体,因此运用该方法可完成这些内容。

  在Rigidbody方面则需删除所有当前速率,然后基于篮球直径采用Ball的碰撞器抵消定位它在玩家手中的位置(注:保证它完全停止运动,不会脱离手中)。

  你可能疑惑 _handTransform变量的来源。还记得自己曾在教程1的场景创建中,在“运动员”手中添加了Box Collider。

  为实现这种愿景,在Awake()函数末端添加如下代码:
  1. _handTransform = _transform.Find (
  2. “BPlayerSkeleton/Pelvis/Hip/Spine/Shoulder_R/UpperArm_R/LowerArm_R/Hand_R”);
复制代码

  这需要参考适当组件,并附加在_transform变量上。另外我们还可公开其属性,并通过之前的编辑器指定变量,但此时最好证明自己可以通过GameObject获得子变量引用。

  一旦运动员拿到球,他应开始运球!

  也就是在拿到球后做出BounceUp动作。如果在Update()期间,游戏处在BouncingBall状态,“运动员”已然抓住球,且Bounce Down动作已完成,那可通过BallBall Rigidbody的AddRelativeForce方法,采用bounceForce变量向下推球。这样,球便会击中地面,并弹回来(因此需要强大力量)。

  为Update方法换上如下代码:
  1. void Update ()
  2. {
  3. if( _holdingBall ){
  4. AttachAndHoldBall();
  5. }
  6. _elapsedStateTime += Time.deltaTime;
  7. }
复制代码

  首先应检查是否已设置_holdingBall。如果是,便可调用AttachAndHoldBall方法定位篮球在运动员手中的位置。

  _holdingBall方法设置为正确,意指调用AttachAndHoldBall方法,设置错误,意指在运球与投篮期间。

  接着在Update()末端添加如下代码:
  1. if( _state == PlayerStateEnum.BouncingBall ){
  2. if( _holdingBall ){
  3. if( GameController.SharedInstance.State == GameController.GameStateEnum.Play && GameController.SharedInstance.TouchDownCount >= 1 ){
  4. State = PlayerStateEnum.PreparingToThrow;
  5. return;
  6. }
  7. }if( _currentAnimation.name.Equals( animBounceDown.name ) ){
  8. if( !_animation.isPlaying && _holdingBall ){
  9. // let go of ball
  10. _holdingBall = false;
  11. // throw ball down
  12. basketBall.BallRigidbody.AddRelativeForce( Vector3.down * bounceForce );
  13. }
  14. }
  15. else if( _currentAnimation.name.Equals( animBounceUp.name ) ){
  16. if( !_animation.isPlaying ){
  17. SetCurrentAnimation( animBounceDown );
  18. }
  19. }
  20. }
复制代码

  上述组块(嵌入到Update方法中)首先检测我们当前是否抓住球,如果答案为肯定,那便询问GameController是否有接触动作。那么便可切换到PrepareToThrow状态,否则需检测运动员的当前动画,以及是否已完成。

  如果完成向下动作,那你便可将球推向地面,如果完成向上动作,那便可开始向下动作。

  当球弹回时,它会碰到运动员手中的Box Collider触发器。此时可调用如下方法:
  1. public void OnTriggerEnter (Collider collider)
  2. {
  3. if (_state == PlayerStateEnum.BouncingBall) {
  4. if (!_holdingBall && collider.transform == basketBall.BallTransform) {
  5. AttachAndHoldBall ();
  6. SetCurrentAnimation (animBounceUp);
  7. }
  8. }
  9. }
复制代码

  这样,在球弹回运动员手中时,便可重新启动弹跳顺序。

  记住,触发事件并不会传送到GameController组件上,Collision事件则会。因此,在发生碰撞事件时,不会自动调用Player Component上的OnTriggerEnter方法。

  然而,你可以编写一个辅助脚本促进实现此动作。新建一个脚本“PlayerBallHand”,输入如下代码:
  1. using UnityEngine;
  2. using System.Collections;[RequireComponent (typeof(Collider))]
  3. public class PlayerBallHand : MonoBehaviour
  4. {private Player _player = null;void Awake ()
  5. {}void Start ()
  6. {
  7. Transform parent = transform.parent;
  8. while (parent != null && _player == null) {
  9. Player parentPlayer = parent.GetComponent<Player>();
  10. if (parentPlayer != null) {
  11. _player = parentPlayer;
  12. } else {
  13. parent = parent.parent;
  14. }
  15. }
  16. }void Update ()
  17. {}void OnTriggerEnter (Collider collider)
  18. {
  19. _player.OnTriggerEnter (collider);
  20. }
  21. }
复制代码

  它的功能是在篮球回到运动员手中时,通知Player Component。

  接着切换到Unity,并将此脚本附加到运动员对象的Hand_R变量上。记住,在教程1中,你已经在该对象上创建了一个碰撞器。

  同时,选择Player对象,设置篮球为公开变量。

  最后,选择BallPhyMat,设置弹力为1,因此篮球有力量向上弹。

  测试

  你已经编写了一些代码,现在应测试一切是否如预想般运作。如之前做法般,更改Start方法为如下状态,测试球的弹力:
State = PlayerStateEnum.BouncingBall;

  同时,不选择GameObject组件,禁用GameController脚本,以免干扰测试。

  接着点击开始按钮;如果一切正常,那你将会看到球来回弹动,如下图所示!

unity3d-bouncing-testfrom-raywenderlich.png
unity3d-bouncing-test(from raywenderlich)

  [注:在重启GameController组件前,你应删除测试代码片段。]

  投篮

  首先应在Player类中标明如下变量:
  1. public float maxThrowForce = 5000f;
  2. public Vector3 throwDirection = new Vector3( -1.0f, 0.5f, 0.0f );
复制代码
  maxThrowForce是指投篮时使用的最大力量,其数值与用户手指在屏幕上停留的时间长短有关(注:比如停留的时间越长,便能使用更大比例的力量)。而throwDirection变量决定了投篮角度。

  接着,在Update()方法末端添加如下代码,保证在适当时间内投篮:
  1. if (_state == PlayerStateEnum.PreparingToThrow) {
  2. if (GameController.SharedInstance.State == GameController.GameStateEnum.Play &&
  3. GameController.SharedInstance.TouchCount == 0) {State = PlayerStateEnum.Throwing;_holdingBall = false;
  4. basketBall.BallRigidbody.AddRelativeForce (
  5. throwDirection *
  6. (maxThrowForce * _animation [animPrepareThrow.name].normalizedTime));
  7. }
  8. }
复制代码
  之前在运球与玩家轻触屏幕时,你已经在更新方法上增加一些代码,将Player状态设置为“PreparingToThrow”。

  现在,你应再次检测自己是否处在这种状态,用户十是否释放手指。你可以根据相应动作所剩的时间,计算投篮所需的力量。

  normlizedTime是Animation State中的一个属性,它表明投篮距离;0.0意指动作处在开始阶段,1.0意味着动作已在进行中。

  接着,添加如下逻辑,控制Throwing状态:
  1. if (_state == PlayerStateEnum.Throwing ) {
  2. // turn on the collider as you want the ball to react to the player is it bounces back
  3. if( !_animation.isPlaying && !_collider.enabled ){
  4. _collider.enabled = true;
  5. }
  6. }
复制代码
  虽然在此状态中由你决定何时完成投篮动作,然而一旦完成,你应开启碰撞器,确保篮球没有滚出角色范围外。

  完成投篮后,运动员需等待GameController指令。并根据其结果做出相应动作。比如,如果球进入篮框,便做出胜利动作;否则是个失望动作。

  在动画结束后(无论是失分还是得分),GameController会随机选择一个新的投篮位置,通知运动员走到此处。

  定位

  在Player类的顶端添加如下变量:
  1. public float walkSpeed = 5.0f;
复制代码

  walkSpeed变量决定了角色移动到新“投篮位置”的速度。

  同时,如果你查看Player类的内部,你会发现之前添加的shotPosition参数。它将决定运动员的投篮位置,且在每次投篮结束后由GameController更新。

  首先应设置投篮位置,在Awake()函数底部添加如下内容:
  1. _shotPosition = _transform.position;
复制代码

  接着,更改ShotPosition获得者/设置者内容为:
  1. public Vector3 ShotPosition{
  2. get{
  3. return _shotPosition;
  4. }
  5. set{
  6. _shotPosition = value;if( Mathf.Abs( _shotPosition.x – _transform.position.x ) < 0.1f ){
  7. State = PlayerStateEnum.BouncingBall;
  8. } else{
  9. State = PlayerStateEnum.Walking;
  10. }
  11. }
  12. }
复制代码

  如上所示,ShotPosition由GameController设置。也就是说,当ShotPosition发生改变时,Player类应检测它是否移动到新位置,如果是,那需其状态改为Walking(否则恢复运球动作)。

  在每次Update()函数中,当运动员逐渐靠拢新位置时,便会启动运球动作(也就是说用户现在可开始另一次投篮)。

  为此,在Update()末端添加如下代码:
  1. if (_state == PlayerStateEnum.Walking) {
  2. Vector3 pos = _transform.position;
  3. pos = Vector3.Lerp (pos, _shotPosition, Time.deltaTime * walkSpeed);
  4. _transform.position = pos;if ((pos – _shotPosition).sqrMagnitude < 1.0f) {
  5. pos = _shotPosition;
  6. if (OnPlayerAnimationFinished != null) {
  7. OnPlayerAnimationFinished (_currentAnimation.name);
  8. }
  9. }
  10. }
复制代码
  值得注意的是,Unity会兼顾对象位置与运动如果你曾开发过游戏,你可能会发现,为了保证游戏在各种设备运作一致,你必须根据时间流逝更新角色位与动作。这可通过Time静态性能deltaTime实现。

  deltaTime是指随着更新进行的时间流逝。为何借此计算画面上的角色运动呢?如果你曾在现代电脑上体验一款老式作品,你可能会注意到其中的角色总在快速移动,无法操控。

  这是因为角色位置的更新并没有与流逝时间挂钩,而是个常数。比如,移动一个50像素对象的距离取决于多个因素,包括处理器速度。然而,在0.5秒内移动50像素对象会致使它在所有平台或处理器上保持稳定运动模式。

  [注:你可能疑惑“插值”的定义,它是指线性地将一个数值插入另一个数值中的数学函数。比如,如果初始值为0,最终值为10,那么线性插入0.5将会获得结果5。你应熟悉使用插值法;以后会经常用到。]

  现在一切均已完成,应进入测试阶段。

  全面测试

  最后应测试整个游戏!点击开始按钮,开始游戏进程,你可能要根据创建方式调节某些方面:

*通过点击抓住游戏区域,释放,完成投篮动作。如果出现失误,你可以更改运动员的ThrowDirection变量,比如X=1,Y=0.75,Z=0。
*再次检测脚本中设置的所有公共连接均与Player、Scroreboard及GameController完美匹配。

MainScene.unityfrom-raywenderlich.png
MainScene.unity(from raywenderlich)

  如果你仍陷入困境,你可以试着使用调试程序查看失误!首先右击“检测器”选项,选择“排错”。而后通过不断点击在MonoDevelop环境中创建一个断点。

  最后,找到“运行附加进程”,选择Unity编辑器。接着当你体验该应用时,它会在碰到断点时终止进程,你便能排除错误!

Assembly-CSharpfrom-raywenderlich.png
Assembly-CSharp(from raywenderlich)

  如果到目前为止一切运行流畅,祝贺你!此时意味着你拥有一款功能完整的3D游戏。

  同时还应花些时间检查代码,本教程的目的是教授你如何基于Unity引擎处理脚本与事件。

  总结

  在完成此教程学习后,你会获得一个简单项目。在Unity中打开此内容,找到FileOpen Project,点击“打开其它”,浏览文件夹。记住场景不会默认下载,你需要打开,搜索ScenesGameScene。

  在本系列教程的第3部分,我们将会探讨如何为主菜单创建一个简单的用户界面。

来自:游戏邦
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

作品发布|文章投稿|广告合作|关于本站|游戏开发论坛 ( 闽ICP备17032699号-3 )

GMT+8, 2025-2-27 02:32

Powered by Discuz! X3.4

Copyright © 2001-2021, Tencent Cloud.

快速回复 返回顶部 返回列表