制作手机unity排行榜制作3D游戏,要想在手机上运行成功,需要写代码吗,如果要,是像写app代码一样的写吗

这是利用Unity引擎开发ios 3D游戏的第二部分教程,前篇教程请。
在第1部分中,你已掌握与Unity相关的概念:
Unity 3D界面
材料与纹理
物理学与碰撞器
现在,场景中的一切看起来相当粗糙,但到目前为止,这都出自Unity的视觉场景设计师之手。也就是说,你还未编写任何代码!
因此在本篇教程中,你将学习利用代码为游戏注入生命,并为场景增加互动与动画的方法!
本篇教程紧跟前文内容。如果你打算从“已知良好”状态的游戏入手,你可以使用我们在教程1中提到的项目。在Unity中打开这一内容,找到File\Open Project,点击“Open Other”,浏览文件夹。记住场景不会默认加载,你需要打开,选择Scenes\GameScene。
现在开始本文内容。
保证所有组件完美配合
在深入探讨代码编写前,烦请您快速浏览下面图表,它显示出游戏中各个组件的功能与职责,以及它们之间的关系:
位于中心位置的是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#脚本”:
在检查器中,你将看到一份默认脚本,内容如下:
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!
注: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子对象拖到右表中的相应位置:
这样便创建出“3DText”子对象与脚本属性的连接。很简单吧。
在继续行动前,我们应确保一切如预期般运作。首先创建一个可更新记分板上时间与分数的新脚本。命名为“ScoreboardTest”,并复制如下代码:
using UnityE
using System.C
public class ScoreBoardTest : MonoBehaviour
{ & & & &
& & public ScoreB
& & public void Start()
& & {
& & & & scoreboard.SetTime( &60& );
& & & & scoreboard.SetPoints( &100& );
& & }
接着点击GameObject\Create Empty,重命名为“ScoreboardTest”,并将此脚本附加到本游戏对象上(GDchina注:采用拖动方式)。接着将场景的记分板与ScoreBoardTest的记分板变量连接,点击开始。
哇,它运行了,你会在上面截图中看到记分板数字不断跳动!如果没有则需回顾之前步骤,查看可能失误。
现在探讨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 {
& & & & get {
& & & & & & return _
& & & & } &
& & }
& & public Rigidbody BallRigidbody {
& & & & get {
& & & & & & return _ &
& & & & }
& & }
& & public SphereCollider BallCollider {
& & & & get {
& & & & & & 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) {
& & & & & & & & OnNet (); & &
& & & & & & } &
& & & & }
& & }
注:由于该对象与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的所有组件,因此在频繁访问的情况下支持本地缓存(GDchina注:比如在调用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 ){
& & & & & & OnNet(); & &
& & & & } &
& & } & & &
上述代码用于检测球体何时入网。还记得在上个教程中,你曾在篮网正下方设置了一个特殊的箱子碰撞器,并保存为触发器吗?
由于这不属于技术“碰撞”,因此会出现OnTriggerEnter()这种单独回调函数(GDchina注:也可以是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 {
& & & & get {
& & & & & & if (_instance == null) {
& & & & & & & & _instance = GameObject.FindObjectOfType (typeof(GameController)) as GameC
& & & & & & }
& & & & & & return _
& & & & }
& & }
& & 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 {
& & public B
& & void Start () {
& & & & ball.OnNet += Handle_OnN
& & }
& & protected void Handle_OnNet(){
& & & & Debug.Log ( &NOTHING BUT NET!!!& );
& & }
而后采取如下方式进行测试:
将“Ball”脚本拖到篮球对象顶端。
新建一个空白游戏对象“GameController”,在此区域中复制GameController脚本。
新建一个空白游戏对象“BallTest”,在此区域中复制BallTest脚本。
点击BallTest对象,将Ball变量改为篮球。
最后,将篮球对象定位在篮框上方,如下图所示:
点击开始,你会看到主机上显示“NOTHING BUT NET!!!”,并伴随一些调试消息!
此时,你已测试出球体脚本能够正确发觉常见碰撞或触发器碰撞,并可推动这些事件分别在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{
& & & & get{
& & & & & & return _shotP
& & & & }
& & & & set{
& & & & & & _shotPosition =
& & }
& & }
& & public enum PlayerStateEnum
& & {
& & & & Idle, & & & & & & & & &
& & & & BouncingBall, & & & & &
& & & & PreparingToThrow, & & &
& & & & Throwing, & & & & & & &
& & & & Score, & & & & & & & & &
& & & & Miss, & & & & & & & & &
& & & & Walking & & & & & & & & &
& & }
& & private PlayerStateEnum _state = PlayerStateEnum.I
& & private float _elapsedStateTime = 0.0f;
& & private Transform _
& & private Animation _
& & private CapsuleCollider _
& & private bool _holdingBall = false;
& & void Awake ()
& & {
& & & & _transform = GetComponent&Transform&();
& & & & _animation = GetComponent&Animation&();
& & & & _collider = GetComponent&CapsuleCollider&();
& & }
& & void Start ()
& & { & & & & & & & & & & & & & & & & & & & & & &
& & }
& & void Update ()
& & { & & & & & & & & & & & &
& & }
& & public bool IsHoldingBall {
& & & & get {
& & & & & & return _holdingB & &
& & & & }
& & }
& & public PlayerStateEnum State {
& & & & get {
& & & & & & return _
& & & & }
& & & & set { & & & & & & & & & & & &
& & & & & & _state =
& & & & & & _elapsedStateTime = 0.0f;
& & & & }
& & }
& & public float ElapsedStateTime {
& & & & get {
& & & & & & return _elapsedStateT &
& & & & }
& & } & & &
GameController主要依据了解何时完成动画,知晓并设置Player的目前状态。在处理动画事件上可调用OnPlayerAnimationFinished()事件。
同时还可采用记录Player各种可能状态的枚举器:包括闲暇、运球、预备投篮、投篮、得分、失分、走动,各个状态均有对应属性。
注意,基于C#语言的属性创建通常如下:
public float MyProperty{
& & get {
& & & & return MyPropertyV
& & }
& & set{
& & & & 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,
& & Paused,
& & GameOver
以下是关于游戏多种状态的解释:
菜单——展示主菜单项
暂停——类似主菜单
开始——用户真正开始游戏进程
结束——完成游戏
状态如同关口,它会根据当前状态阻止某些路径(依据代码分支)。状态逻辑则贯穿在该类的所有方法中,但设置其为公开属性可用于控制状态切换。
接着为游戏状态添加获取者与设置者,如下所示:
public GameStateEnum State {
& & get{
& & & & return _ &
& & }
& & set{
& & & & _state =
& & & & // MENU
& & & & if( _state == GameStateEnum.Menu ){
& & & & & & Debug.Log( &State change - Menu& ); & & & & & & & &
& & & & & & player.State = Player.PlayerStateEnum.BouncingB
& & & & & & // TODO: replace play state with menu (next tutorial)
& & & & & & StartNewGame();
& & & & } & & & & &
& & & & // PAUSED
& & & & else if( _state == GameStateEnum.Paused ){
& & & & & & Debug.Log( &State change - Paused& ); & & & & & & & & & & & & & & &
& & & & & & // TODO; add pause state (next tutorial)& & & & & & & &
& & & & } & & & & &
& & & & // PLAY
& & & & 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(); & &
& & } else{
& & & & State = GameStateEnum.P
& & }
它类似StartNewGame方法,但具备额外检查功能。在思考整个游戏方案后可调用此方法。否则GameController状态会切换回Play状态,即重新开始。
接下来,为GamePoints确定一个新属性:
public int GamePoints{
& & get{
& & & & return _gameP & &
& & }
& & set{
& & & & _gamePoints =
& & & & scoreBoard.SetPoints( _gamePoints.ToString() );
& & }
它主要用于更新记分板分数。
最后,添加一个TimeRemaining属性:
public float TimeRemaining {
& & get{
& & & & return _timeR
& & }
& & set{& & & & & &
& & & & _timeRemaining =
& & & & scoreBoard.SetTime( _timeRemaining.ToString(&00:00&) ); & & & & & &
& & & & // 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
& & & & } else{ & & & & & &
& & & & & & // 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
& & } else{ & & & & & &
& & & & // 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对象,你会发现其中某些运动员、记分板与篮球的属性为公开模式。通过拖动在“检查器”中设置它们为适当对象。
如今,当你点击开始按钮时,你会发现时间会以秒单位更新。
处理用户输入
很多时候,你会发现自己基于台式机开发游戏,而后在项目接近尾声时,常常会被把它移植到实际设备上。结果,你需要处理这两种输入模式:一是触屏模式,二是键盘与鼠标。
为此,首先应在GameController上添加辅助方法,检测该应用是否可在移动设备上运行:
public bool IsMobile{
& & get{
& & & & return (Application.platform == RuntimePlatform.IPhonePlayer || Application.platform == RuntimePlatform.Android); &
& & }
幸好该设计无需过多互动;所有必要条件都可决定手指是否在落在屏幕上,以下代码片段便具有此作用。
public int TouchCount {
& & get{
& & & & if( IsMobile ){
& & & & & & return Input.touchC
& & & & } else{
& & & & & & // 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) ){
& & & & & & & & return 1; &
& & & & & & } else{
& & & & & & & & return 0;
& & & & & & }
& & & & }
& & } &
public int TouchDownCount {
& & get{
& & & & if( IsMobile ){
& & & & & & int currentTouchDownCount = 0;
& & & & & & foreach( Touch touch in Input.touches ){
& & & & & & & & if( touch.phase == TouchPhase.Began ){
& & & & & & & & & & currentTouchDownCount++; & &
& & & & & & & & }
& & & & & & }
& & & & & & return currentTouchDownC
& & & & } else{
& & & & & & if( Input.GetMouseButtonDown(0) ){
& & & & & & & & return 1; &
& & & & & & } else{
& & & & & & & & return 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便会看到如下情况:
其中有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
通过变量引用动画能够灵活便捷地更新动画,无需依赖特定动画文件或索引/名称。
当然,为实现该理念,你不得不对应配合动画与脚本组件中的各个公开属性,那就马上行动吧:
接下来是创建各个动画,此时可调用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{
& & get{
& & & & return _animation.isP & &
& & }
public AnimationClip CurrentAnimation {
& & get {
& & & & return _currentA
& & }
& & set {
& & & & 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
点击开始按钮:如果一切运作顺畅,那你便会看到篮球运动员做出“预备投篮”动作!
注:在重新启动GameController脚本前,应删除测试代码片段。
现在应充实State属性(之前已创建完毕);但在此之前,我们应剔除以下必要方法。
我们会在探讨篮球运动员如何运球方面详细解释此方法。但现在应将之前的State属性替换成以下代码片段:
private void AttachAndHoldBall(){
大部分代码都是有关基于当前设置状态,调用SetCurrentAnimation方法,设置适当动画。我们应注意某些重点代码:
public PlayerStateEnum State{
& & get{
& & & & return _
& & }
& & set{
& & & & CancelInvoke(&OnAnimationFinished&);
& & & & _state =
& & & & _elapsedStateTime = 0.0f;
& & & & switch( _state ){
& & & & case PlayerStateEnum.Idle:
& & & & & & SetCurrentAnimation( animIdle ); & && & & & & &
& & & & & & break;& & & & &
& & & & case PlayerStateEnum.BouncingBall:
& & & & & & _collider.enabled = false;
& & & & & & AttachAndHoldBall(); & && & & &
& & & & & & SetCurrentAnimation( animBounceUp );& & & & & & & &
& & & & & & break;
& & & & case PlayerStateEnum.PreparingToThrow:
& & & & & & SetCurrentAnimation( animPrepareThrow );
& & & & & & break;
& & & & case PlayerStateEnum.Throwing:& & & & & & &
& & & & & & SetCurrentAnimation( animThrow );
& & & & & & break;
& & & & case PlayerStateEnum.Score:
& & & & & & SetCurrentAnimation( animScore );
& & & & & & break;
& & & & case PlayerStateEnum.Miss:
& & & & & & SetCurrentAnimation( animMiss );
& & & & & & break;
& & & & case PlayerStateEnum.Walking:
& & & & & & if( _shotPosition.x & _transform.position.x ){
& & & & & & & & SetCurrentAnimation( animWalkForward );
& & & & & & } else{
& & & & & & & & SetCurrentAnimation( animWalkBackward );
& & & & & & }
& & & & & & break;
& & & & } & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & &
& & }
比如首个语句:
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 = true;
& & 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的碰撞器抵消定位它在玩家手中的位置(GDchina注:保证它完全停止运动,不会脱离手中)。
你可能疑惑 _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
& & & & & & return;
& & & & }
& & }
& & if( _currentAnimation.name.Equals( animBounceDown.name ) ){
& & & & if( !_animation.isPlaying && _holdingBall ){
& & & & & & // let go of ball
& & & & & & _holdingBall = false; &
& & & & & & // 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
& & & & & & } else {
& & & & & & & & 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脚本,以免干扰测试。
接着点击开始按钮;如果一切正常,那你将会看到球来回弹动,如下图所示!
注:在重启GameController组件前,你应删除测试代码片段。
首先应在Player类中标明如下变量:
public float maxThrowForce = 5000f;
public Vector3 throwDirection = new Vector3( -1.0f, 0.5f, 0.0f );
maxThrowForce是指投篮时使用的最大力量,其数值与用户手指在屏幕上停留的时间长短有关(GDchina注:比如停留的时间越长,便能使用更大比例的力量)。而throwDirection变量决定了投篮角度。
接着,在Update()方法末端添加如下代码,保证在适当时间内投篮:
if (_state == PlayerStateEnum.PreparingToThrow) {
& & if (GameController.SharedInstance.State == GameController.GameStateEnum.Play &&
& & & & GameController.SharedInstance.TouchCount == 0) {
& & & & State = PlayerStateEnum.T
& & & & _holdingBall = false;
& & & & 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 = true;
& & }
虽然在此状态中由你决定何时完成投篮动作,然而一旦完成,你应开启碰撞器,确保篮球没有滚出角色范围外。
完成投篮后,运动员需等待GameController指令。并根据其结果做出相应动作。比如,如果球进入篮框,便做出胜利动作;否则是个失望动作。
在动画结束后(无论是失分还是得分),GameController会随机选择一个新的投篮位置,通知运动员走到此处。
在Player类的顶端添加如下变量:
public float walkSpeed = 5.0f;
walkSpeed变量决定了角色移动到新“投篮位置”的速度。
同时,如果你查看Player类的内部,你会发现之前添加的shotPosition参数。它将决定运动员的投篮位置,且在每次投篮结束后由GameController更新。
首先应设置投篮位置,在Awake()函数底部添加如下内容:
_shotPosition = _transform.
接着,更改ShotPosition获得者/设置者内容为:
public Vector3 ShotPosition{
& & get{
& & & & return _shotP
& & }
& & set{
& & & & _shotPosition =
& & & & if( Mathf.Abs( _shotPosition.x - _transform.position.x ) & 0.1f ){
& & & & & & State = PlayerStateEnum.BouncingB &
& & & & } else{
& & & & & & 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完美匹配。
如果你仍陷入困境,你可以试着使用调试程序查看失误!首先右击“检测器”选项,选择“排错”。而后通过不断点击在MonoDevelop环境中创建一个断点。
最后,找到“运行\附加进程”,选择Unity编辑器。接着当你体验该应用时,它会在碰到断点时终止进程,你便能排除错误!
如果到目前为止一切运行流畅,祝贺你!此时意味着你拥有一款功能完整的3D游戏。
同时还应花些时间检查代码,本教程的目的是教授你如何基于Unity引擎处理脚本与事件。
在完成此教程学习后,你会获得一个简单项目。在Unity中打开此内容,找到File\Open Project,点击“打开其它”,浏览文件夹。记住场景不会默认下载,你需要打开,搜索Scenes\GameScene。
在本系列教程的第3部分,我们将会探讨如何为主菜单创建一个简单的用户界面。

我要回帖

更多关于 unity制作全景图 的文章

 

随机推荐