c 游戏开发发采用Unity3D+.NET Core是一种什么样的体验

教程:如何使用Unity制作3D版iOS游戏(2)
发布时间: 11:40:58
作者:Joshua Newnham
这是使用Unity制作3D游戏教程系列的第2部分!(请点击此处查看)
在第1部分中,你已掌握与Unity相关的概念:
*Unity 3D界面
*材料与纹理
*物理学与碰撞器
现在,场景中的一切看起来相当粗糙,但到目前为止,这都出自Unity的视觉场景设计师之手。也就是说,你还未编写任何代码!
因此在本篇教程中,你将学习利用代码为游戏注入生命,并为场景增加互动与动画的方法!
本篇教程紧跟前文内容。如果你打算从“已知良好”状态的游戏入手,你可以使用我们在教程1中提到的项目。在Unity中打开这一内容,找到File\Open Project,点击“Open Other”,浏览文件夹。记住场景不会默认加载,你需要打开,选择Scenes\GameScene。
现在开始本文内容。
保证所有组件完美配合
在深入探讨代码编写前,烦请您快速浏览下面图表,它显示出游戏中各个组件的功能与职责,以及它们之间的关系:
class-diagram-1()
位于中心位置的是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#脚本”:
Project(from raywenderlich)
在检查器中,你将看到一份默认脚本,内容如下:
using UnityE
using System.C
public class DummyScript : MonoBehaviour {
// Use this for initialization
void Start () {
// Update is called once per frame
void Update () {
上面的Start()与Update()方法便是钩子方法;也称为“记号”,即在更新每一帧时被调用。游戏引擎的一个核心性能是不断更新与渲染循环。每当移动对象,场景便会重新渲染。对象再次移动,再次渲染。如此循环。
首次实例化一个“组件”时会调用Awake()方法。一旦它用于所有活跃“组件”,随后便会调用Start()方法。接着是在更新每一帧或“记号”时会调用Update()方法。
[注:MonoBehaviour中还有另一种更新方法为FixedUpdate()。它由物理引擎调用,仅在更新Rigidbody或其它物理属性时使用。之所以称其为FixedUpdate(),是因为它能保证固定间隔的调用,不像Update()方法在每次“记号”时调用,而记号间的时间并不固定。]
首先从编写ScoreBoard脚本入手,其实该脚本编写相当简单。你已经创建了一个脚本,因此只需重命名为“ScoreBoard”,双击打开。
相信你还不知道Unity引擎中包括MonoDevelop!
ScoreBoard(from raywenderlich)
[注:MonoDevelop是指针对C#语言开发,探讨本教程范围外所有性能与功能的完整集成开发环境。但如果你只局限于编辑与保存文件也无大碍。更多先进性能可在MonoDevelop中找到。]
在新脚本中插入如下代码:
using UnityE
using System.C
public class ScoreBoard : MonoBehaviour
public TextMesh pointsTextM
public TextMesh timeRemainingTextM
void Awake ()
void Start ()
void Update ()
public void SetTime (string timeRemaining)
timeRemainingTextMesh.text = timeR
public void SetPoints (string points)
pointsTextMesh.text =
上面脚本介绍了公开访问属性的概念。此时的属性是指“记分板”子对象中3D Text的“分数”与“时间”。
公开这些属性意味着它们在“检查器”面板中可视,那样你便可在设计时间内通过编辑器指定它们。一旦完成,你便可以调用SetTime()与SetPoints()的设置方法修改文本属性。
完成上面的脚本创建后,你应切换回Unity,将它附加到“记分板”对象上。只要拖动脚本对象到“记分板”顶端便可完成。
接着,将教程1中的各个3D Text子对象拖到右表中的相应位置:
Scripting-ScoreBoard(from raywenderlich)
这样便创建出“3DText”子对象与脚本属性的连接。很简单吧。
在继续行动前,我们应确保一切如预期般运作。首先创建一个可更新记分板上时间与分数的新脚本。命名为“ScoreboardTest”,并复制如下代码:
using UnityE
using System.C
public class ScoreBoardTest : MonoBehaviour
public ScoreB
public void Start()
scoreboard.SetTime( + );
scoreboard.SetPoints( + );
接着点击GameObject\Create Empty,重命名为“ScoreboardTest”,并将此脚本附加到本游戏对象上(游戏邦注:采用拖动方式)。接着将场景的记分板与ScoreBoardTest的记分板变量连接,点击开始。
unity3d-scoreboard-test(from raywenderlich)
哇,它运行了,你会在上面截图中看到记分板数字不断跳动!如果没有则需回顾之前步骤,查看可能失误。
现在探讨Unity引擎控制物体碰撞的方式。
之前说过,Ball的功能是在其通过篮框或落地时通知GameController。而且其上面附有Sphere Collider与Rigidbody,用于检测并对碰撞做出反应。在此脚本中,你可以通过倾听碰撞声正确通知GameController。
如同之前做法,新建一个脚本“Ball”。而后在MonoDevelop环境中编辑如下代码:
using UnityE
using System.C
[RequireComponent (typeof(SphereCollider))]
[RequireComponent (typeof(Rigidbody))]
public class Ball : MonoBehaviour
private Transform _
private Rigidbody _
private SphereCollider _sphereC
public delegate void Net ();
public Net OnNet =
private GameController _gameC
void Awake ()
_transform = GetComponent&Transform&();
_rigidbody = GetComponent&Rigidbody&();
_sphereCollider = GetComponent&SphereCollider&();
void Start ()
_gameController = GameController.SharedI
void Update ()
public Transform BallTransform {
public Rigidbody BallRigidbody {
public SphereCollider BallCollider {
return _sphereC
public void OnCollisionEnter (Collision collision)
_gameController.OnBallCollisionEnter (collision);
public void OnTriggerEnter (Collider collider)
if (collider.transform.name.Equals (“LeftHoop_001″)) {
if (OnNet != null) {
[注:由于该对象与GameController相互依存,因此有必要删除某些必要方法,之后实行空白标记法。此种情况下不能调用OnBallCollision()实例方法与ShareInstance()类方法。]
以下是以代码块形式概述此脚本中引入的新概念:
[RequireComponent (typeof (SphereCollider))]
[RequireComponent (typeof (Rigidbody))]
Unity提供的类属性支持你在类中增加设计时间逻辑。此时,你应告知Unity引擎,此脚本依赖SphereCollider,而且RigidBody已附加到该脚本上。
这是种良好习惯,尤其在项目规模不断扩大之际,它有助于自动为脚本中增添附加“组件”,避免出现不必要的漏洞。
private Transform _
private Rigidbody _
private SphereCollider _sphereC
void Awake ()
_transform = GetComponent&Transform&();
_rigidbody = GetComponent&Rigidbody&();
_sphereCollider = GetComponent&SphereCollider&();
GetComponent()方法继承自MonoBehaviour的类,前者能针对某个特定组件类型搜索局部GameObject。没有则返回null,反之回到Component。由于这需要研究GameObject的所有组件,因此在频繁访问的情况下支持本地缓存(游戏邦注:比如在调用Update()或FixedUpdate()方法时会有所需要)。
private GameController _gameC
void Start () {
_gameController = GameController.SharedI
由于此对象涉及GameController,因此可通知诸如碰撞这类事件。Game Controller将会是单例模式,可通过SharedInstance静态属性使用。
public delegate void Net();
public Net OnNet =
如果之前你从未使用过C#语言,那你便不大熟悉委托与事件。从根本上说,它们能够方便Component之间的交流。此时,外部Component会在OnNet事件中记录兴趣,借此随着Ball脚本催生出OnNet事件不断更新结果(实行观察者模式)。此概念极其类似iOS编程中采用的委托模式。
public void OnCollisionEnter( Collision collision ){
_gameController.OnBallCollisionEnter( collision );
Ball的主要任务是在其通过篮网或落地时通知GameController。由于其上面附有Rigidbody,因此当它与地面的BoxCollider碰撞时,物理引擎会通过OnCollisionEnter()方法发送消息。
此Collision参数会传递出更多相关细节,包括球体的碰撞对象。此时,只需将在GameController上输入此细节便可知晓解决方案。
[注:除了OnCollisionEnter()方法,你还可调用OnCollisionStay()与OnCollisionExit()方法,即在对象与其它物体发生碰撞或停止碰撞的每一帧时调用。更多详情可登陆 /Documentation/ScriptReference/MonoBehaviour.html.Unity官方文档查询。]
public void OnTriggerEnter( Collider collider ) {
if( collider.transform.name.Equals ( “LeftHoop_001″ ) ) {
if( OnNet != null ){
上述代码用于检测球体何时入网。还记得在上个教程中,你曾在篮网正下方设置了一个特殊的箱子碰撞器,并保存为触发器吗?
由于这不属于技术“碰撞”,因此会出现OnTriggerEnter()这种单独回调函数(游戏邦注:也可以是OnTriggerStay()与OnTriggerExit()),通常在与触发器碰撞时调用。
此时,你需检查碰撞事件中的参与对象。如果它碰巧是篮网触发器,那可以通过OnNet方法通知GameController。
相关做法如上所示!记住,在Unity中,你还不能将脚本附加到篮球对象上,因为此脚本依赖于你还未创建的GameController对象。
上述内容着重球体方面,现在应考虑到Player!但在此之前我们应确保一切如预期般运作。
首先,应为Ball脚本依附的GameController脚本创建一个存根,借此测试所有内容。因此新建一个脚本“GameController”,并替代如下内容:
using UnityE
using System.C
public class GameController : MonoBehaviour {
private static GameController _instance =
public static GameController SharedInstance {
if (_instance == null) {
_instance = GameObject.FindObjectOfType (typeof(GameController)) as GameC
void Awake() {
_instance =
public void OnBallCollisionEnter (Collision collision) {
Debug.Log ( “Game Controller: Ball collision occurred!” );
这便是Singleton模式。它从属设计模式,能够保证系统中仅存在单个对象实例,类似全程变量或Highlander。
因此,其它类便能较易访问GameController。作为一个连接完整的对象,它方便其它对象之间的互动,检查目前系统状态。
[注:如果你十分好奇其它iOS项目中采用的Singleton模式,你可以在Stack Overflow中找到更多关于在iOS 4.1系统中实行单例模式的探讨内容。]
为实现Singleton模式,调用静态方法可回到共享实例。如果还未设置共享实例,那可使用GameObjects的FindObjectOfType()静态法查找,这样可在场景中获得首个活动对象。
[注:在执行Singleton模式时,通常设置构造函数为隐藏模式,因此其存取器可控制实例。由于是继承自Unity MonoBehaviour的类,因此我们无法将构造函数设置为隐藏模式。所以这是个隐含Singleton模式,程序员必须明确执行。]
接着增加一个测试脚本,测试球体的所有碰撞行为,类似之前的记分板测试。
为此,新建一个“BallTest”脚本,复制如下代码:
using UnityE
public class BallTest : MonoBehaviour {
void Start () {
ball.OnNet += Handle_OnN
protected void Handle_OnNet(){
Debug.Log ( “NOTHING BUT NET!!!” );
而后采取如下方式进行测试:
*将“Ball”脚本拖到篮球对象顶端。
*新建一个空白游戏对象“GameController”,在此区域中复制GameController脚本。
*新建一个空白游戏对象“BallTest”,在此区域中复制BallTest脚本。
*点击BallTest对象,将Ball变量改为篮球。
最后,将篮球对象定位在篮框上方,如下图所示:
unity3d-ball-test-1(from raywenderlich)
点击开始,你会看到主机上显示“NOTHING BUT NET!!!”,并伴随一些调试消息!
debug messages(from raywenderlich)
此时,你已测试出球体脚本能够正确发觉常见碰撞或触发器碰撞,并可推动这些事件分别在OnNet处理器与GameController上进行。
现在已清楚碰撞事件可正当运作,接着可以设置运动员!
运动员框架
现在,你只需执行Player代码存根。在完成GameController设置后可回到此阶段。
新建一个“Player”脚本,在MonoDevelop环境中编辑如下代码:
using UnityE
using UnityE
using System.C
[RequireComponent (typeof(Animation))]
public class Player : MonoBehaviour
public delegate void PlayerAnimationFinished (string animation);
public PlayerAnimationFinished OnPlayerAnimationFinished =
private Vector3 _shotPosition = Vector3.
public Vector3 ShotPosition{
return _shotP
_shotPosition =
public enum PlayerStateEnum
BouncingBall,
PreparingToThrow,
private PlayerStateEnum _state = PlayerStateEnum.I
private float _elapsedStateTime = 0.0f;
private Transform _
private Animation _
private CapsuleCollider _
private bool _holdingBall =
void Awake ()
_transform = GetComponent&Transform&();
_animation = GetComponent&Animation&();
_collider = GetComponent&CapsuleCollider&();
void Start ()
void Update ()
public bool IsHoldingBall {
return _holdingB
public PlayerStateEnum State {
_elapsedStateTime = 0.0f;
public float ElapsedStateTime {
return _elapsedStateT
GameController主要依据了解何时完成动画,知晓并设置Player的目前状态。在处理动画事件上可调用OnPlayerAnimationFinished()事件。
同时还可采用记录Player各种可能状态的枚举器:包括闲暇、运球、预备投篮、投篮、得分、失分、走动,各个状态均有对应属性。
注意,基于C#语言的属性创建通常如下:
public float MyProperty{
return MyPropertyV
MyPropertyValue =
借此便能清晰便捷地控制“获得者”与“设置者”。
记住,拖动“等级系统”面板上玩家对象顶端的Player脚本。
这样便创建了Player,实现这个存根相当简单,我们命名该运动员为“Stubby”。接着是设置GameController!
游戏控制器
GameController的功能是协调游戏中的活动,接受用户输入。
那么“协调活动”意味着什么?通常游戏的运作与状态机器一样。其当前状态能够决定运行哪部分代码,如何中断用户输入,以及屏幕前后的情况。
在复杂游戏中,你常常会把各个状态封装到一个实体上,但在简单游戏中最好采用枚举法与语句切换控制各种游戏状态。
这时便为GameController创建一个启动器脚本,现在我们开始内部构造。
首先应标明所需变量。由于GameController主要用于协调所有物体,因此你需要参考大部分用于控制游戏统计数据的变量(比如当前得分、剩余时间等)。
添加如下代码,标明变量(相关注释已插入到代码片段中):
public P // Reference to your player on the scene
public ScoreBoard scoreB // Reference to your games scoreboard
public Ball basketB // reference to the courts one and only basketball
public float gameSessionTime = 180.0f;
// time for a single game session (in seconds)
public float throwRadius = 5.0f; // radius the player will be positioned for each throw
private GameStateEnum _state = GameStateEnum.U
// state of the current game – controls how user interactions are interrupted and what is activivated and disabled
private int _gamePoints = 0; // Points accumulated by the user for this game session
private float _timeRemaining = 0.0f; // The time remaining for current game session
// we only want to update the cou so we’ll accumulate the time in this variable
// and update the remaining time after each second
private float _timeUpdateElapsedTime = 0.0f;
// The original player position – each throw position will be offset based on this and a random value
// between-throwRadius and throwRadius
private Vector3 _orgPlayerP
公开gameSessionTime(运动员的游戏时间)与throwRadius(篮球运动员可能需移动的距离)意味着在测试阶段方便调整。
你已为Player状态增加了一些状态;现在可以为游戏添加状态:
public enum GameStateEnum
Undefined,
以下是关于游戏多种状态的解释:
*菜单——展示主菜单项
*暂停——类似主菜单
*开始——用户真正开始游戏进程
*结束——完成游戏
状态如同关口,它会根据当前状态阻止某些路径(依据代码分支)。状态逻辑则贯穿在该类的所有方法中,但设置其为公开属性可用于控制状态切换。
接着为游戏状态添加获取者与设置者,如下所示:
public GameStateEnum State {
if( _state == GameStateEnum.Menu ){
Debug.Log( “State change – Menu” );
player.State = Player.PlayerStateEnum.BouncingB
// TODO: replace play state with menu (next tutorial)
StartNewGame();
else if( _state == GameStateEnum.Paused ){
Debug.Log( “State change – Paused” );
// TODO; add pause state (next tutorial)
else if( _state == GameStateEnum.Play ){
Debug.Log( “State change – Play” );
// GAME OVER
else if( _state == GameStateEnum.GameOver ){
Debug.Log( “State change – GameOver” );
// TODO; return user back to the menu (next tutorial)
StartNewGame();
将此状态封装在一个属性内,那样你便能轻易拦截状态更改,必要时执行必要逻辑(如上图所示)。
支持方法与属性
接着可以增加一些支持方法与属性。
首先添加如下所示的StartNewGame方法:
public void StartNewGame(){
GamePoints = 0;
TimeRemaining = gameSessionT
player.State = Player.PlayerStateEnum.BouncingB
State = GameStateEnum.P
该方法主要用于重新设置游戏统计数据(上面标明的变量),为新游戏场景准备实体。
接着添加ResumeGame方法:
public void ResumeGame(){
if( _timeRemaining & 0 ){
StartNewGame();
State = GameStateEnum.P
它类似StartNewGame方法,但具备额外检查功能。在思考整个游戏方案后可调用此方法。否则GameController状态会切换回Play状态,即重新开始。
接下来,为GamePoints确定一个新属性:
public int GamePoints{
return _gameP
_gamePoints =
scoreBoard.SetPoints( _gamePoints.ToString() );
它主要用于更新记分板分数。
最后,添加一个TimeRemaining属性:
public float TimeRemaining {
return _timeR
_timeRemaining =
scoreBoard.SetTime( _timeRemaining.ToString(&#″) );
// reset the elapsed time
_timeUpdateElapsedTime = 0.0f;
保证记分板能根据当前剩余时间更新分数。
实行支持方法与属性后,时间会处在最新状态!
保证一切处于最新状态
现在应着眼于如何让GameController记录相应情况,此时可调用Update方法与追踪方式。在该组件上添加如下代码:
void Update () {
if( _state == GameStateEnum.Undefined ){
// if no state is set then we will switch to the menu state
State = GameStateEnum.M
else if( _state == GameStateEnum.Play ){
UpdateStatePlay();
else if( _state == GameStateEnum.GameOver ){
UpdateStateGameOver();
private void UpdateStatePlay(){
_timeRemaining -= Time.deltaT
// accumulate elapsed time
_timeUpdateElapsedTime += Time.deltaT
// has a second past?
if( _timeUpdateElapsedTime &= 1.0f ){
TimeRemaining = _timeR
// after n seconds of the player being in the miss or score state reset the position and session
if( (player.State == Player.PlayerStateEnum.Miss || player.State == Player.PlayerStateEnum.Score)
&& player.ElapsedStateTime &= 3.0f ){
// check if the game is over
if( _timeRemaining &= 0.0f ){
State = GameStateEnum.GameO
// set a new throw position
Vector3 playersNextThrowPosition = _orgPlayerP
// offset x
playersNextThrowPosition.x +=
Random.Range(-throwRadius, throwRadius);
player.ShotPosition = playersNextThrowP
private void UpdateStateGameOver(){
// TODO; to implement (next tutorial)
Update方法是根据当前状态把任务委托到特定方法上。正如你所看到的,UpdateStatePlay方法的代码片段上涉及的代码,详情如下。
_timeRemaining -= Time.deltaT
// accumulate elapsed time
_timeUpdateElapsedTime += Time.deltaT
// has a second past?
if( _timeUpdateElapsedTime &= 1.0f ){
TimeRemaining = _timeR
第一部分用于更新游戏运作时间(或剩余时间)。使用 _timeUpdateElapsedTime变量追踪TimeRemaining属性的最后一次更新,将此更新速度降为以秒为单位,因为快速更新记分板(通过TimeReamining属性实现)没多大必要,可能会影响游戏性能。
// after n seconds of the player being in the miss or score state reset the position and session
if( (player.State == Player.PlayerStateEnum.Miss || player.State == Player.PlayerStateEnum.Score)
&& player.ElapsedStateTime &= 3.0f ){
// check if the game is over
if( _timeRemaining &= 0.0f ){
State = GameStateEnum.GameO
// set a new throw position
Vector3 playersNextThrowPosition = _orgPlayerP
// offset x
playersNextThrowPosition.x +=
Random.Range(-throwRadius, throwRadius);
player.ShotPosition = playersNextThrowP
第二片段则用于检查篮球运动员何时完成投篮动作,游戏是否结束。如果他在3秒左右的时间内一直处在Miss或Score状态,那便意味着完成一次投篮。之所以有所延误,是因为在开始下一个投篮前,你希望有个动画来圆满此事件。
接着应检查是否有时间剩余。如果没有,可将状态调整为GameOver,否则应指使篮球运动员移动到一个新位置,开始另一次投篮。
之前,你已在“等级系统”中创建了一个GameController对象,并附加相应脚本,可见相应工作可告一个段落。
在“等级系统”面板中选择GameController对象,你会发现其中某些运动员、记分板与篮球的属性为公开模式。通过拖动在“检查器”中设置它们为适当对象。
Inspector(from raywenderlich)
如今,当你点击开始按钮时,你会发现时间会以秒单位更新。
unity3d-game-controller-test-1(from raywenderlich)
处理用户输入
很多时候,你会发现自己基于台式机开发游戏,而后在项目接近尾声时,常常会被把它移植到实际设备上。结果,你需要处理这两种输入模式:一是触屏模式,二是键盘与鼠标。
为此,首先应在GameController上添加辅助方法,检测该应用是否可在移动设备上运行:
public bool IsMobile{
return (Application.platform == RuntimePlatform.IPhonePlayer || Application.platform == RuntimePlatform.Android);
幸好该设计无需过多互动;所有必要条件都可决定手指是否在落在屏幕上,以下代码片段便具有此作用。
public int TouchCount {
if( IsMobile ){
return Input.touchC
// if its not consdered to be mobile then query the left mouse button, returning 1 if down or 0 if not
if( Input.GetMouseButton(0) ){
public int TouchDownCount {
if( IsMobile ){
int currentTouchDownCount = 0;
foreach( Touch touch in Input.touches ){
if( touch.phase == TouchPhase.Began ){
currentTouchDownCount++;
return currentTouchDownC
if( Input.GetMouseButtonDown(0) ){
为了确定用户是否接触屏幕,你可以根据该应用的运作平台,分别使用TouchCount与TouchDownCount属性。
如果是在移动平台上运行,通过查询(返回到)“输入”类型,检测触屏数量即可,否则便可断定该作在台式机上运作,查询MouseButton的输入量(点击结果为1,否则为0)。
TouchCount与TouchDownCount两者的唯一区别是,前者计算手指在屏幕上的滑动次数,并没有考虑其发生阶段,而后者只计算开始阶段中的滑动次数。
[注:这种Touch类有个枚举法名为TouchPhase,一个触摸阶段基本上等同于当前触摸状态,比如,首次发觉(即手指首次触摸屏幕)触屏则制定为Began阶段,一旦滑动便是Moved阶段,拿开手指则为Ended阶段。]
若想充分了解Unity的Input类,可参照Unity官方网站(/Documentation/ScriptReference/Input.html).
控制球体发送的消息
回想起来,Ball会在两种情况下向GameController发送消息:一是它进入篮框,二是击中地面。
调用OnBallCollisionEnter方法处理篮球碰撞地面的情况:
public void OnBallCollisionEnter (Collision collision)
if (!player.IsHoldingBall) {
if ((collision.transform.name == “Ground” ||
collision.transform.name == “Court”) &&
player.State == Player.PlayerStateEnum.Throwing) {
player.State = Player.PlayerStateEnum.M
OnBallCollisionEnter()函数能够检测运动员是否抓住球。如果没有,那便断定球已投出。因此,如果球碰到地面或出界,那便表示此回合结束。如果它碰到地面或球场,且未投中篮框,那便可设置运动员的状态为Miss。
Ball Component与HandleBasketBallOnNet事件会调用此函数。那该如何连接两者?可以在OnNet事件中记录‘兴趣’,并调用Start()方法。
同时还可以将它们添加到新方法Start()上,此处十分适合放置初始化代码:
void Start () {
// register the event delegates
basketBall.OnNet += HandleBasketBallOnN
这便是为事件委托指定回调函数的方法。在Ball催生出Net事件时可调用HandleBasketBallOnNet方法。
其实现方法如下:
public void HandleBasketBallOnNet(){
GamePoints += 3;
player.State = Player.PlayerStateEnum.S
控制来自运动员组件的消息
另一与GameController互动的组件是Player。此时并没有将其考虑在内,但在这节中你将处理GameController上的消息与事件。Player会在动画结束后提出一个事件,反过来会触发GameController上游戏动态的更新。
在Start()方法末尾添加如下代码,用于记录事件:
player.OnPlayerAnimationFinished += HandlePlayerOnPlayerAnimationF
以及附加方法:
public void HandlePlayerOnPlayerAnimationFinished (string animationName)
if (player.State == Player.PlayerStateEnum.Walking) {
player.State = Player.PlayerStateEnum.BouncingB
在运动员完成走路动作后,此代码会将其状态更改为BouncingBall。
接下来,教程会将所有事件结合起来,保证你最终会投中几个篮框!
运动员的必要功能:
以下快速回顾了运动员的职责与必要功能:
*在闲暇时间,运动员可以运球。
*在Play状态时,运动员应对用户输入做出反应;此时,如若用户手指紧紧放在屏幕上,运动员便会‘汇聚力量’准备投篮。
*运动员会影响到篮球位置与其性能。
*运动员应在每回合结束后绕球场移动。
*运动员应根据当前状态有所表现,比如在球进入篮框后做出胜利动作,在错失时做出失望动作。
*在完成上述动作后,运动员应通知GameController。
接着回过头来打开Player脚本,仔细浏览里面代码。
Unity提供了一系列丰富类能用于处理3D动画包的导入与使用。在导入Blender中创建的运动员时会附加一系列动画。选择编辑器中PlayerObject的Animation Component便会看到如下情况:
player-animations(from raywenderlich)
其中有10个时段,每个时段又包含一个Animation Clip。点击任意一个Animation Clip,便可播放脚本中的任何动画。
[注:有关Animation Component的更多内容可以查看Unity官方文档:/Documentation/Components/class-Animation.html]
在Player脚本中,添加一些变量可控制当前动画,AnimationClips主要参考如下动画:
private AnimationClip _currentAnimation =
public AnimationClip animI
public AnimationClip animBounceD
public AnimationClip animBounceUp;
public AnimationClip animWalkF
public AnimationClip animWalkB
public AnimationClip animPrepareT
public AnimationClip animT
public AnimationClip animS
public AnimationClip animM
通过变量引用动画能够灵活便捷地更新动画,无需依赖特定动画文件或索引/名称。
当然,为实现该理念,你不得不对应配合动画与脚本组件中的各个公开属性,那就马上行动吧:
Inspector(from raywenderlich)
接下来是创建各个动画,此时可调用Player Start方法(同时参照附加动画组件)。添加如下代码:
void Start(){
_animation = GetComponent&Animation&();
InitAnimations();
private void InitAnimations ()
_animation.Stop ();
_animation [animIdle.name].wrapMode = WrapMode.O
_animation [animBounceDown.name].wrapMode = WrapMode.O
_animation [animBounceUp.name].wrapMode = WrapMode.O
_animation [animWalkForward.name].wrapMode = WrapMode.L
_animation [animWalkBackward.name].wrapMode = WrapMode.L
_animation [animPrepareThrow.name].wrapMode = WrapMode.O
_animation [animThrow.name].wrapMode = WrapMode.O
_animation [animScore.name].wrapMode = WrapMode.O
_animation [animMiss.name].wrapMode = WrapMode.O
_animation [animBounceDown.name].speed = 2.0f;
_animation [animBounceUp.name].speed = 2.0f;
Animation Component实则是动画的控制器与贮存器。每个动画都包含在AnimationState类中。你可以通过索引定位或关键字进行使用,此处的关键字是指动画名称。你可以在上面的编辑器截屏中看到。
比如动画中的两个属性:即wrapMode与速度。后者决定特定动画的重播速度,而前者决定动画的‘包装’模式;也就是说,每个回合结束后的动画场面。此处的动画要么只上演一次,要么会循环反复。
接下来只剩下播放动画!在Player类中添加如下代码:
public bool IsAnimating{
return _animation.isP
public AnimationClip CurrentAnimation {
return _currentA
SetCurrentAnimation (value);
public void SetCurrentAnimation (AnimationClip animationClip)
_currentAnimation = animationC
_animation [_currentAnimation.name].time = 0.0f;
_animation.CrossFade (_currentAnimation.name, 0.1f);
if (_currentAnimation.wrapMode != WrapMode.Loop) {
Invoke (“OnAnimationFinished”, _animation [_currentAnimation.name].length /
_animation [_currentAnimation.name].speed);
private void OnAnimationFinished ()
if (OnPlayerAnimationFinished != null) {
OnPlayerAnimationFinished (_currentAnimation.name);
上述代码展示出有关动画控制的所有方法。主要是SetCurrentAnimation()方法。
此时,重设当前动画时间为O(即回到开始),那么Animation Component需要交叉渐变出特定动画。交叉渐变会随着当前动画呈现而消失。也就是说,当前动画会逐渐‘散开’,平缓过渡到新动画上。
此后,应检查动画是否会循环反复。如果没有,便可采用Invoke方法推迟调用OnAnimationFinished()函数。而这要推迟到动画结束。
最后,OnAnimationFinied()函数的功能是催生出连锁事件,从而通知GameController动画已经完成,从而知晓Player GameObject的当前状态与动作。
我们应保证所有动画的设置与运行没有差池。为此,在Player启动方法末端添加下面代码:
接着,不选择GameObject组件,禁用GameController脚本:
CurrentAnimation = animPrepareT
点击开始按钮:如果一切运作顺畅,那你便会看到篮球运动员做出“预备投篮”动作!
unity3d-disable-the-gamecontroller(from raywenderlich)
[注:在重新启动GameController脚本前,应删除测试代码片段。]
现在应充实State属性(之前已创建完毕);但在此之前,我们应剔除以下必要方法。
我们会在探讨篮球运动员如何运球方面详细解释此方法。但现在应将之前的State属性替换成以下代码片段:
private void AttachAndHoldBall(){
大部分代码都是有关基于当前设置状态,调用SetCurrentAnimation方法,设置适当动画。我们应注意某些重点代码:
public PlayerStateEnum State{
CancelInvoke(“OnAnimationFinished”);
_elapsedStateTime = 0.0f;
switch( _state ){
case PlayerStateEnum.Idle:
SetCurrentAnimation( animIdle );
case PlayerStateEnum.BouncingBall:
_collider.enabled =
AttachAndHoldBall();
SetCurrentAnimation( animBounceUp );
case PlayerStateEnum.PreparingToThrow:
SetCurrentAnimation( animPrepareThrow );
case PlayerStateEnum.Throwing:
SetCurrentAnimation( animThrow );
case PlayerStateEnum.Score:
SetCurrentAnimation( animScore );
case PlayerStateEnum.Miss:
SetCurrentAnimation( animMiss );
case PlayerStateEnum.Walking:
if( _shotPosition.x & _transform.position.x ){
SetCurrentAnimation( animWalkForward );
SetCurrentAnimation( animWalkBackward );
比如首个语句:
CancelInvoke(“OnAnimationFinished”);
该语句要求Unity取消排队调用OnAnimationFinished方法,你可能十分熟悉此方法,因为在上演非循环动画时曾用过。
接下来的有趣代码片段是PlayerStateEnum.Walking;在此组块中,你会基于目标位置决定相应动画,而不是基于当前位置决定篮球运动员是否前进或后退。
类似上面做法,快速检测角色状态与动作是否完美匹配。在Player类的Start方法中添加如下代码:
State = PlayerStateEnum.S
如之前那般,不选择GameObject组件,禁用GameController脚本,以免干扰测试。
点击开始按钮;如果一切运作顺畅,那你将会看到篮球运动员做出“得分”动作(在投篮成功时做出的动作)。
[注:在重启GameController脚本前,应删除测试代码片段。]
篮球运动员在等待用户输入期间的一个职责是运球。本部分中我们将揭示实现这种动作所需的代码与设置。
首先,在Player类顶端标明变量:
public Ball basketB
public float bounceForce = 1000f;
private Transform _handT
变量_handTransform主要参照Transform组件的接触球,而bounceForce则用于决定运球所需的力量(篮球变量应相当明显)。
问题是,当Player状态改为BouncingBall时,我们该如何定位此球在玩家手中的位置。此时可调用之前剔除的AttachAndHoldBall方法:
public void AttachAndHoldBall ()
_holdingBall =
Transform bTransform = basketBall.BallT
SphereCollider bCollider = basketBall.BallC
Rigidbody bRB = basketBall.BallR
bRB.velocity = Vector3.
bTransform.rotation = Quaternion.
Vector3 bPos = bTransform.
bPos = _handTransform.
bPos.y -= bCollider.
bTransform.position = bP
其中一个公开变量与篮球对象有关。其功能需参照球体转变、碰撞与刚体,因此运用该方法可完成这些内容。
在Rigidbody方面则需删除所有当前速率,然后基于篮球直径采用Ball的碰撞器抵消定位它在玩家手中的位置(游戏邦注:保证它完全停止运动,不会脱离手中)。
你可能疑惑 _handTransform变量的来源。还记得自己曾在教程1的场景创建中,在“运动员”手中添加了Box Collider。
为实现这种愿景,在Awake()函数末端添加如下代码:
_handTransform = _transform.Find (
“BPlayerSkeleton/Pelvis/Hip/Spine/Shoulder_R/UpperArm_R/LowerArm_R/Hand_R”);
这需要参考适当组件,并附加在_transform变量上。另外我们还可公开其属性,并通过之前的编辑器指定变量,但此时最好证明自己可以通过GameObject获得子变量引用。
一旦运动员拿到球,他应开始运球!
也就是在拿到球后做出BounceUp动作。如果在Update()期间,游戏处在BouncingBall状态,“运动员”已然抓住球,且Bounce Down动作已完成,那可通过BallBall Rigidbody的AddRelativeForce方法,采用bounceForce变量向下推球。这样,球便会击中地面,并弹回来(因此需要强大力量)。
为Update方法换上如下代码:
void Update ()
if( _holdingBall ){
AttachAndHoldBall();
_elapsedStateTime += Time.deltaT
首先应检查是否已设置_holdingBall。如果是,便可调用AttachAndHoldBall方法定位篮球在运动员手中的位置。
_holdingBall方法设置为正确,意指调用AttachAndHoldBall方法,设置错误,意指在运球与投篮期间。
接着在Update()末端添加如下代码:
if( _state == PlayerStateEnum.BouncingBall ){
if( _holdingBall ){
if( GameController.SharedInstance.State == GameController.GameStateEnum.Play && GameController.SharedInstance.TouchDownCount &= 1 ){
State = PlayerStateEnum.PreparingToT
if( _currentAnimation.name.Equals( animBounceDown.name ) ){
if( !_animation.isPlaying && _holdingBall ){
// let go of ball
_holdingBall =
// throw ball down
basketBall.BallRigidbody.AddRelativeForce( Vector3.down * bounceForce );
else if( _currentAnimation.name.Equals( animBounceUp.name ) ){
if( !_animation.isPlaying ){
SetCurrentAnimation( animBounceDown );
上述组块(嵌入到Update方法中)首先检测我们当前是否抓住球,如果答案为肯定,那便询问GameController是否有接触动作。那么便可切换到PrepareToThrow状态,否则需检测运动员的当前动画,以及是否已完成。
如果完成向下动作,那你便可将球推向地面,如果完成向上动作,那便可开始向下动作。
当球弹回时,它会碰到运动员手中的Box Collider触发器。此时可调用如下方法:
public void OnTriggerEnter (Collider collider)
if (_state == PlayerStateEnum.BouncingBall) {
if (!_holdingBall && collider.transform == basketBall.BallTransform) {
AttachAndHoldBall ();
SetCurrentAnimation (animBounceUp);
这样,在球弹回运动员手中时,便可重新启动弹跳顺序。
记住,触发事件并不会传送到GameController组件上,Collision事件则会。因此,在发生碰撞事件时,不会自动调用Player Component上的OnTriggerEnter方法。
然而,你可以编写一个辅助脚本促进实现此动作。新建一个脚本“PlayerBallHand”,输入如下代码:
using UnityE
using System.C
[RequireComponent (typeof(Collider))]
public class PlayerBallHand : MonoBehaviour
private Player _player =
void Awake ()
void Start ()
Transform parent = transform.
while (parent != null && _player == null) {
Player parentPlayer = parent.GetComponent&Player&();
if (parentPlayer != null) {
_player = parentP
parent = parent.
void Update ()
void OnTriggerEnter (Collider collider)
_player.OnTriggerEnter (collider);
它的功能是在篮球回到运动员手中时,通知Player Component。
接着切换到Unity,并将此脚本附加到运动员对象的Hand_R变量上。记住,在教程1中,你已经在该对象上创建了一个碰撞器。
同时,选择Player对象,设置篮球为公开变量。
最后,选择BallPhyMat,设置弹力为1,因此篮球有力量向上弹。
你已经编写了一些代码,现在应测试一切是否如预想般运作。如之前做法般,更改Start方法为如下状态,测试球的弹力:
State = PlayerStateEnum.BouncingB
同时,不选择GameObject组件,禁用GameController脚本,以免干扰测试。
接着点击开始按钮;如果一切正常,那你将会看到球来回弹动,如下图所示!
unity3d-bouncing-test(from raywenderlich)
[注:在重启GameController组件前,你应删除测试代码片段。]
首先应在Player类中标明如下变量:
public float maxThrowForce = 5000f;
public Vector3 throwDirection = new Vector3( -1.0f, 0.5f, 0.0f );
maxThrowForce是指投篮时使用的最大力量,其数值与用户手指在屏幕上停留的时间长短有关(游戏邦注:比如停留的时间越长,便能使用更大比例的力量)。而throwDirection变量决定了投篮角度。
接着,在Update()方法末端添加如下代码,保证在适当时间内投篮:
if (_state == PlayerStateEnum.PreparingToThrow) {
if (GameController.SharedInstance.State == GameController.GameStateEnum.Play &&
GameController.SharedInstance.TouchCount == 0) {
State = PlayerStateEnum.T
_holdingBall =
basketBall.BallRigidbody.AddRelativeForce (
throwDirection *
(maxThrowForce * _animation [animPrepareThrow.name].normalizedTime));
之前在运球与玩家轻触屏幕时,你已经在更新方法上增加一些代码,将Player状态设置为“PreparingToThrow”。
现在,你应再次检测自己是否处在这种状态,用户十是否释放手指。你可以根据相应动作所剩的时间,计算投篮所需的力量。
normlizedTime是Animation State中的一个属性,它表明投篮距离;0.0意指动作处在开始阶段,1.0意味着动作已在进行中。
接着,添加如下逻辑,控制Throwing状态:
if (_state == PlayerStateEnum.Throwing ) {
// turn on the collider as you want the ball to react to the player is it bounces back
if( !_animation.isPlaying && !_collider.enabled ){
_collider.enabled =
虽然在此状态中由你决定何时完成投篮动作,然而一旦完成,你应开启碰撞器,确保篮球没有滚出角色范围外。
完成投篮后,运动员需等待GameController指令。并根据其结果做出相应动作。比如,如果球进入篮框,便做出胜利动作;否则是个失望动作。
在动画结束后(无论是失分还是得分),GameController会随机选择一个新的投篮位置,通知运动员走到此处。
在Player类的顶端添加如下变量:
public float walkSpeed = 5.0f;
walkSpeed变量决定了角色移动到新“投篮位置”的速度。
同时,如果你查看Player类的内部,你会发现之前添加的shotPosition参数。它将决定运动员的投篮位置,且在每次投篮结束后由GameController更新。
首先应设置投篮位置,在Awake()函数底部添加如下内容:
_shotPosition = _transform.
接着,更改ShotPosition获得者/设置者内容为:
public Vector3 ShotPosition{
return _shotP
_shotPosition =
if( Mathf.Abs( _shotPosition.x – _transform.position.x ) & 0.1f ){
State = PlayerStateEnum.BouncingB
State = PlayerStateEnum.W
如上所示,ShotPosition由GameController设置。也就是说,当ShotPosition发生改变时,Player类应检测它是否移动到新位置,如果是,那需其状态改为Walking(否则恢复运球动作)。
在每次Update()函数中,当运动员逐渐靠拢新位置时,便会启动运球动作(也就是说用户现在可开始另一次投篮)。
为此,在Update()末端添加如下代码:
if (_state == PlayerStateEnum.Walking) {
Vector3 pos = _transform.
pos = Vector3.Lerp (pos, _shotPosition, Time.deltaTime * walkSpeed);
_transform.position =
if ((pos – _shotPosition).sqrMagnitude & 1.0f) {
pos = _shotP
if (OnPlayerAnimationFinished != null) {
OnPlayerAnimationFinished (_currentAnimation.name);
值得注意的是,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.unity(from raywenderlich)
如果你仍陷入困境,你可以试着使用调试程序查看失误!首先右击“检测器”选项,选择“排错”。而后通过不断点击在MonoDevelop环境中创建一个断点。
最后,找到“运行\附加进程”,选择Unity编辑器。接着当你体验该应用时,它会在碰到断点时终止进程,你便能排除错误!
Assembly-CSharp(from raywenderlich)
如果到目前为止一切运行流畅,祝贺你!此时意味着你拥有一款功能完整的3D游戏。
同时还应花些时间检查代码,本教程的目的是教授你如何基于Unity引擎处理脚本与事件。
在完成此教程学习后,你会获得一个简单项目。在Unity中打开此内容,找到File\Open Project,点击“打开其它”,浏览文件夹。记住场景不会默认下载,你需要打开,搜索Scenes\GameScene。
在本系列教程的第3部分,我们将会探讨如何为主菜单创建一个简单的用户界面。(本文为游戏邦/编译,拒绝任何不保留版权的转载,如需转载请联系:游戏邦)
Intermediate Unity 3D for iOS: Part 2/3
by Joshua Newnham
This is a tutorial by Joshua Newnham, the founder of We Make Play, an independent studio crafting creative digital play for emerging platforms.
Welcome back to our Intermediate Unity 3D for iOS tutorial series!
In this tutorial series, you are learning how to create a simple 3D game in Unity called “Nothing but Net”. In the first part of the tutorial, you learned about the following Unity concepts:
The Unity3D Interface
Materials and Textures
Scene Positioning
Camera Positioning
Physics and Colliders
Everything in your scene looks pretty sharp, but so far everything you’ve done has been through Unity’s visual scene designer. In other words, you haven’t written any code yet!
Well, that’s about to change :] In this part of the tutorial, you’ll start to breathe some life into your game through code, and add some interaction and animation into the scene!
This tutorial picks up where the previous tutorial left off. If you want to start at a “known good” state, you can use the project where we left it off in the previous tutorial. To open it in Unity, go to File\Open Project, click Open Other, and browse to the folder. Note that the scene won’t load by default – to open it, select Scenes\GameScene.
Let’s get started!
Making Sure Everybody Plays Together Nicely
Before you get too deep into code, take a quick look at the diagram below, which shows the functionality and responsibility of each component you’ll be adding to the game, as well as the relationships between the components:
At the center is the GameController. This is an abstract GameObject meaning that it is not associated to any physical element on the stage, but rather used to manage the scene. In this instance the GameController is responsible for coordinating the various states of the game activities providing access to the input from the user.
The next Component is the ScoreBoard. This encapsulates methods to update the points and time 3D Text GameObjects in the scene.
Next is the Player who is responsible for responding to user input, and managing the various properties of the ball, including the ball’s position.
Finally, what would your game be without the Ball? This object is responsible for triggering specific events that indicate when the Ball has gone through the hoop, and when the Ball has landed on the ground, signifying that the player’s turn is over.
Scripting, Scripting, Scripting
Unity provides a choice of several different these include Boo (no, I’m not trying to scare you, it’s actually a language!), Javascript (a.k.a. UnityScript), and C#. In general, if you’re coming from a front-end web development background, then UnityScript is probably the best choice.
However, if you are more comfortable with C++, Java, Objective-C, or C#, then C# is a better choice for your scripting tasks. Since most of the readers of this site have an Objective-C background,in this tutorial you’ll be writing your scripts in C#.
Each script will be a Component unto itself, which will be attached to a GameObject. The base class you will be extending is the MonoBehaviour and includes a whole set of pre-defined properties, methods, and hooks.
Note: Are you wondering what “hooks” are? Hooks are callbacks or messages that are propagated to all Components for certain events, one example that we’ll be using is the OnTriggerEnter method which is called when two Colliders intersect each other (where on has the Is Trigger flag set to true but more on this later).
Let’s try this out! In the Project panel, select the Scripts folder, click Create, and click C# Script:
In the Inspector, you will see that it has created a default script for you that looks similar to the following:
The Start() and Update() methods above are k they are called by the engine during each frame update, also known as a “tick”. One of the core behaviors of a game engine is the continuous update and render cycle. An object is moved, then the scene is re-rendered. The object is moved again, and the scene is rendered once more. Lather, rinse, repeat! :]
When a Component is first instantiated, the Awake() method (which isn’t listed here, but you can override it) will be called. Once the Awake() method has been called on all active Components, then the Start() method will be called. The Update() method is called next, and will be called during each frame update or “tick”.
ScoreBoard
Start off with the ScoreBoard script, which is fairly straightforward. You’ve already created a the script, so just rename it to Scoreboard, then double-click it to open it.
Aha! Bet you didn’t know Unity included MonoDevelop!
Note: MonoDevelop is a full IDE for C# development and discussing all of the features and functionality is out of scope for this tutorial. However, if you limit what you do to editing and saving files, you’ll do just fine. If you want to use more advanced features, you can find out more about MonoDevelop at MonoDevelop.
Insert the code below into the new script:
The script above introduces the concept of publicly accessible properties. In this case, those properties are the Points and Time 3D Text objects that are children of your ScoreBoard object.
Making these properties public means they will become visible in the Inspector panel, which will allow you to assign them at design time via the editor. Once the properties have been assigned, you’ll be able to modify their text properties via the setter methods SetTime() and SetPoints().
Once you’ve created the script above, switch back to Unity and attach it to the ScoreBoard object. To do this, simply drag the script object and drop it on top of the Scoreboard object.
Next, drag and drop each of the 3D Text child objects from Part 1 of this tutorial onto the appropriate property in the right hand column as shown below:
This associates the 3DText child objects with the script property. Pretty simple, eh?
Time to test
Before continuing, lets make sure everything is functioning as expected. To do this we’ll create a new script that will update the time and score of your scoreboard. Create a new script called ScoreboardTest and copy the following code into the script:
Next click GameObject\Create Empty, rename the object to ScoreboardTest, and attach your ScoreBoardTest script to it (again by dragging it on top). Then link up the scene’s scoreboard GameObject with the ScoreBoardTest scoreboard variable and press play.
w00t it works – you should see the text on the scoreboard change as in the above screenshot! If not, trace back your steps to see what might have gone wrong.
Controlling Collisions
Now it’s time to take a look at how Unity handles object collisions.
Recall that the Ball object is responsible for notifying the GameController when it goes through the net and/or hits the ground. The Ball object is attached to a Sphere Collider and Rigidbody ,which allows you to detect and react to collisions. In your script, you can listen for those collisions and notify the Game Controller appropriately.
Like you did before, create a new script and name it Ball. Then edit it in MonoDevelop as follows:
Note: Because of dependences between objects and the GameController, several required methods need to be stubbed out with an empty method signature that will be implemented later. In this case, the OnBallCollision() instance method and SharedInstance() class method have been stubbed out.
Here’s an overview of the new concepts introduced in this script, code block by code block:
Unity provides class attributes that allow you to add design time logic to your classes. In this case you’re telling Unity that this script is dependent on a SphereCollider and RigidBody Components being attached to this script.
This is a good habit to get into, especially when your projects start growing in size as it provides a way of automatically adding dependent Components to your script thus avoiding unnecessary bugs.
The GetComponent() method is inherited by the MonoBehaviour class and provides a way of searching the local GameObject for a specific type of Component. If none exists then null is returned otherwise the Component is returned. Because it requires traversing through the GameObject’s components is always a good idea to cache them locally when frequent access is required (such as when you need them in either the Update() or FixedUpdate() methods).
This object needs a reference to the Game Controller, so it can notify it of events such as collisions. The Game Controller will be a singleton, and you’ll be able to access it via the SharedInstance static property.
If you haven’t worked with C# before, the delegate and event may look a little foreign. Essentially, they provide a method of communication between Components. In this case, the external Component would register interest in the OnNet event, and in doing so, will be updated when an OnNet event is raised by the Ball script (implementation of the Observer pattern). This is a very similar concept to the delegate pattern used in iOS programming.
The main task of the Ball is to notify GameController each time the Ball goes through the net and/or hits the ground. Since the Ball has a Rigidbody attached, when it collides with the BoxCollider Component of the Ground object, the Physics engine will send notification via the OnCollisionEnter() method.
The Collision parameter passed in gives details about the collision, including which object the Ball collided with. In this case, just pass the details onto the GameController to figure out what to do with it. :]
The above code detects when the ball passes through the net. Remember how in the last tutorial you set up a special box collider right below the net and saved it as a trigger?
Well, since this is not technically a “collision”, there is a separate callback named OnTriggerEnter() (as well as OnTriggerStay() and OnTriggerExit()) that gets called when a collision with a trigger occurs.
Here, you check which object is involved with the collision event. If it happens to be the hoop’s trigger, then you let the GameController know about it via the OnNet method discussed above.
And that’s it! Note you can’t attach the script to the basketball object in Unity yet, because this script depends on the GameController object, which you haven’t created yet.
That takes care of the ball — but now the Player needs to play a part in this game! :] But before we do lets make sure everything is working as expected.
Time to test
First things first, you need to create a stub for the GameController script that the Ball script depends on so you can test everything out. So create a new script called GameController and replace the contents with the following:
This is known as a Singleton. A Singleton is a design pattern which ensures that only a single instance of this object exists in your system, much like a global variable or Highlander — there can be only one.
The is done so that the GameController is easily accessible by other classes. As a well-connected object, it provides a central way for other objects to communicate with each other and check what state the system is in.
To implement the Singleton, provide a static method that returns the shared instance. If the shared instance has not been set, then look for it using the GameObjects static method FindObjectOfType() which will return the first active object of the type you’ve requested in the scene.
Next, you’ll add a test script to test out the ball’s collision behavior, similar to how you tested the scoreboard earlier.
To do this, ceate a new script called BallTest and copy the following code:
Then hook everything up by doing the following:
Drag the Ball script on top of the basketball object.
Create a new empty GameObject called GameController and drag the GameController script on it.
Create a new empty GameObject and rename it to BallTest and drag the BallTest script on it.
Click the BallTest object, and drag the basketball onto the Ball variable.
Finally, position the basketball GameObject over the net as shown below:
Then click play, and you should see the output “NOTHING BUT NET!!!” into your console (Window\Console), along with a few other debug messages!
Here you have tested that the ball script correctly detects normal collisions or trigger collisions, and can forward those events on to an OnNet handler or the GameController, respectively.
Now you know that collisions are working properly, you can proceed to setting up the player!
Player Framework
For now, you’ll just implement the Player code as a stub. You’ll return to this later on, once the GameController has been completed.
Create a new script named Player and edit it in MonoDevelop as follows:
The GameController is dependent on knowing when an animation has finished and is able to set and get the current state of the Player. To handle animation events, there is an
OnPlayerAnimationFinished() event.
There is also an enumerator for each possible state of the Player: Idle, BouncingBall, PreparingToThrow, Throwing, Score, Miss, and Walking, and there is a property to get the current state.
Note that properties in C# are created with the following format:
This provides a nice, clean way of managing Getters and Setters.
Before you forget, drag the Player script on top of the player object in the Hierarchy panel to attach it.
That’s it for the Player object is now – this stub implementation is so simple that we could nickname the basketball player “Stubby” :] Let’s move onto implementing more of the GameController!
GameController
The GameController is responsible for coordinating the activities of the game, as well as accepting user input.
What does “coordinating the activities” mean? Games normally function as a state machine. The current state of the game will determine what section of code runs, how user input is interrupted,and what happens on-screen and behind the scenes.
In a complex game, you’d normally encapsulate each state in its own entity, but given the simplicity of this game, it will be sufficient to use an enumeration and switch statement to handle the various game states.
You already created a starter script for the GameController – let’s start building it up.
You’ll start by declaring the variables you need. Since the GameController’s main job is to act as a co-ordinator between all game entities, you need reference to the majority of them all along with variables used to manage the game statistics (e.g. current score, time remaining, etc).
Add the following code to declare the variables (comments embedded within the code snippet):
Exposing gameSessionTime (how long the player has to score) and throwRadius (how far your basketball player will potentially move either side of his current/starting position) as public means you can tweak them easily during play testing.
Game States
You’ve added some states for your P now add some states for the game:
Here’s an explanation of the various game states:
Menu – display your main menu items
Pause – present a similar menu to the main menu
Play – when the user is actually playing the game
Game Over – when play is finished
States acts as gates deterring what path to take (in terms of code branching) based on the current state. The state logic (in this project) is used through-out the methods of this class but exposes itself as a public property which is used to manage the switching of the states.
Next add a getter and setter for the game state, as follows:
Encapsulating the state within a property allows you to easily intercept changes to states and perform necessary logic as required (as you can see above).
Support methods and properties
Next you’ll add some supporting methods and properties.
First add the StartNewGame method as follows:
This method is responsible for resetting the game statistics (variables declared above) and preparing the game entities on your scene for a new game.
Next add the ResumeGame method:
This is similar to the StartNewGame but performs additional checks. If the game is considered over (time has ran out) then StartNewGame, will be called. Otherwise you switch the GameController state back to Play to resume game play.
Next, define a new property for GamePoints:
This will be responsible for keeping your scoreboard update-to-date with the latest current score.
Finally, add a TimeRemaining property:
This is responsible for keeping the scoreboard update-to-date with the current amount of time remaining.
Done with support methods and properties – time for the big guy, Update!
Keeping everything up to date
Now you’ll shift your focus to what makes the GameController tick, the Update method and accompanying methods. Add the following code to GameController:
The Update method delegates the task to a specific method based on what the current state is. As you can see the bulk of the code in this code snippet belongs to the UpdateStatePlay method, let’s go through it bit by bit.
The first part is responsible for updating the elapsed game time (or time remaining). You track the last time you updated the TimeRemaining property using the variable _timeUpdateElapsedTime, throttling updates to every second as updating your scoreboard any quicker (which is done via the TimeReamining property) is not necessary (we are not showing milliseconds) and could potentially affect performance.
The next section is responsible for checking for when the basketball player has finished a throw and checking if the game is finished. The basketball player is considered having finished a throw when he has been in either the Miss or Score state for 3 or more seconds. The reason for the delay is that you want an animation to finish before moving onto the next throw.
You then check if there is any time remaining. If not, you update the state to GameOver, otherwise you ask the basketball player to move into a new position for another shot.
Time to test
You already created a GameController object in the Hierarchy earlier and attached the GameController script to it, so you’re all set there.
Select the GameController object in the Hierarchy panel, and you’ll notice the GameController now has public properties for the player, scoreboard, and basketball. Set those to the appropriate objects in the Inspector by dragging and dropping.
Now when you click on the play button you will see the time update as the GameController reduces the time every second!
Handling User Input
In many cases you’ll find yourself developing on your desktop machine, and porting over to an actual device with greater frequency as the project nears completion. Therefore you need to handle input in both contexts: from the device touchscreen, as well as the keyboard and mouse.
To do this, first add a helper method into GameController to detect whether the app is running on a mobile device or not:
Luckily your design requires all that is required is determining if a finger is down or not, the following snippet does just that.
To determine if the user is touching the screen or not you use the TocuhCount and TouchDownCount properties that branch depending on what platform the app is running on.
If it’s running on the mobile platform then you query (and return) the Input class for the number of touches that have been detected, otherwise you assume we are running on your desktop and query the Input class if the MouseButton is down (returning 1) or not (returning 0).
The only difference between TouchCount and TouchDownCount is that TouchCount counts the number of fingers present on the screen regardless of their phase while TouchDownCount only counts those fingers whose phase is set to Began.
Node:The Touch class has an enumeration called TouchPhase, a touch phase is essentially the current state of the touch e.g. when first detected (when the finger first touches the screen) the touch will be assigned a Began phase, once moving the touch will be assigned the Moved phase, and when finally lifted off the touch will be assigned the Ended phase.
For a full overview of Unity’s Input class, please refer to the official Unity site (/Documentation/ScriptReference/Input.html).
Ball Handling: Dealing With Messages
Recall that the Ball object sends messages to GameController in the case of two events: when the ball goes through the net, and when it hits the ground.
Replace the OnBallCollisionEnter method to handle the case when the ball collides with the ground:
OnBallCollisionEnter() checks to see if the player is holding the ball. If not, then it’s assumed the ball has been thrown. Therefore, if the ball collides with the ground or the sides of the court, then the turn is over. If the ball hits the ground or the court and the player missed the hoop, then set the player state to Miss.
OnBallCollisionEnter() is called explicitly by the Ball Component and its HandleBasketBallOnNet event. How do you associate OnBallCollisionEnter() with the event from HandleBasketBallOnNet? You do this in the Start() method by registering ‘interest’ in the OnNet event.
You should add this in a new method called Start(), which is a good place to put initialization code like this:
This is how you assign a callback to the event delegates. When the Ball raises the Net event, the HandleBasketBallOnNet method will be called.
Then add the implementation for HandleBasketBallOnNet as follows:
Handling Messages from the Player Component
The other Component that communicates with the GameController is the Player. At this point the Player is only stubbed out, but here you’ll implement the handling of messages and events in the GameController. The Player raises an event when an animation is finished playing, which in turn triggers an update in the GameController’s game state.
Add the following code to the end of the Start() method to register for the event:
Along with its accompanying method:
This code updates the state of the Player to BouncingBall once he has finished walking.
The next portion of this tutorial will tie all of these events together and allow you to finally shoot a few hoops! :]
The Player: “Stubby” No More!
Here’s a quick review of what the Player is responsible for and what functionality will be required:
During idle time, the Player should bounce the ball.
When in the Play game state, the Player should in this case when the user holds their finger on the screen, the Player will ‘wind’ up for the throw.
The Player should affect the position of the Ball and influence its behavior.
The Player should move around the court after each turn.
The Player should animate based on the current state, e.g. show a winning animation when the ball goes through the hoop and a disappointed animation when the ball misses.
The Player should notify the GameController when each of the above animations completes.
Head back and open up the Player script, and lets slowly make your way through the code.
Character animation
Unity provides a rich set of classes that handle the importing and use of animations from 3D packages. When you imported the player that was created in Blender, it came packaged with a set of animations. If you select the Animation Component of the Player object in the editor you’ll see the following:
The Animation Component has 10 slots, each containing a separate Animation Clip. You can play any of these animations in script by asking the Animation Component to play a specific Animation Clip.
Inside the Player script, add some variables to manage the current animation and AnimationClips that will reference the various animations:
Referencing the animations via a variable provides the flexibility to easily update the animations without having to rely on a specific animation file or index/name.
Of course, for this to work you have to connect the appropriate animation to each public property on the script component, so go ahead and do that now:
The next step is to set up each of the animations. You do this by calling a dedicated method from the Player Start method (along with getting reference to the attached animation Component. Add the following code to do this:
The Animation Component acts as a controller and repository for your animations. Each animation is wrapped in a class called AnimationState. You can access each one by index position or key, where key is the name of the animation. This is shown visually in the editor screenshot above.
Take a look at two properties in each animation: wrapMode and speed. Speed determines the playback speed for the specific animation, whereas wrapMode determines how the animation is ‘wrapped’; in other words, what the animation does once it has come to the end. Here the animations are either being played just once, or are looping.
The only thing left to do is to play the animations! :] Add the following code to the Player class:
The above code shows all the methods associated with handling animation. The main method is SetCurrentAnimation().
Here the current animation time is reset to 0 (i.e. back to the start) and then the Animation Component is requested to crossFade the specified animation. Cross fading permits fading in an animation over the currently playing animation over the specified time. This means the current animation will be slowly ‘blended’ out to give a smooth transition between the current and new animation.
After requesting the animation to play via crossFade, check if the animation is a loop. If not, then request a delayed call to OnAnimationFinished() using the Invoke method. The call will be delayed by the time of the animation.
Finally, OnAnimationFinished() is responsible for raising the associated event, and thus notifying the GameController that an animation has finished so that it is aware of the current state and actions of the Player GameObject.
Time to test
Lets make sure your animations are all set up and running correctly. To do this, add the following line to the end of your Player’s start method:
Then disable the GameController script by unchecking the GameObjects component (as shown below):
And clic if all is well then you will see the basketball player play the “prepare to throw” animation! :]
Note: Before continuing remember to enable the GameController again and remove the test code snippet.
Managing state
Time to flesh out your State property (that you created previously); but before you do that lets stub out a method that you need.
This method will be explained when we talk through how the basketball player bounces the ball. For now, replace your previous State property with the follow code snippet:
Most of the code is related to setting up the appropriate animation based on the currently set state, using your SetCurrentAnimation method we created above. So lets concentrate on the less trivial code.
One of the first statements is:
This statement asks Unity to cancel any invoke that maybe queued up called OnAnimationFinished, which should look pretty familiar to you because we created this invoke when playing a non-looping animation.
The next interesting piece of code is for the state PlayerStateEnum.W in this block you are determining the animation based on the target (shot) position compared to your current position to determine whether the basketball player is walking backwards or forwards.
Time to test
Similar to above, let’s perform a quick test to check that your states and animations are working correctly together. Add the following code to the Start method of your Player class:
As you did before, disable the GameController script by unchecking the GameObjects component so that it does not interfere with your test.
And clic if all is well then you will see your basketball player plays the “score” animation (the animation that will run when the user successfully gets a ball in the hoop).
Note:Before continuing remember to enable the GameController again and remove the test code snippet.
Bouncing the Ball
One responsibility of the basketball player is to bounce the ball while waiting for user input. In this section we will cover the code and setup required to make this

我要回帖

更多关于 c 游戏开发 的文章

 

随机推荐