此篇是一个国外教程的翻译,虽然有点老,但是适合新手入门。自己去写代码,debug,布置场景,可以收获到很多。游戏邦上已经有前面两部分的译文,这里翻译的是游戏的最后一个部分。
在第一篇中,我们学会了怎么在Unity中搭建游戏的场景,并且设置模型的物理属性。
在第二篇中,我们学会了怎么在unity中使用脚本,并且创建了大部分的游戏逻辑,包括投球和得分!
在这最后一节中,我们将会为用户创建一个菜单系统,并且和GameController进行交互,我们开始吧。
到目前为止,我们只在unity内建的模拟器中进行了测试。现在你的项目能够正常运行了,是时候让他在真实的设备里跑跑测试了。
点击菜单栏 File->Build Setting.你后悔看到下面的对话框:
首先,确定你选择对了平台(iOS旁边应该有一个unity的标志,如果没有,选择iOS,然后点Switch Platform)
选择Player Settings,在inspector面板中就可以对游戏进行编译设置。
(译注:Android平台的运行可以参考 - Unity3D游戏开发从零单排(一) - 真机运行)
这里有一大堆可以设置的东西,但是现在你真正需要关心的是保证游戏的屏幕方向是正确的。在Resoluion and Presentation面板中的devices orientation的下拉选框中选择Landscape-Left,其他保持默认,接下来是Other Settings。
在这个部分你需要输入你的developer Bundle Identifier(和在XCode里面一样),余下的部分保持默认。
当你设置好编译的依稀选项之后,回到Build Setting对话框,点Build。
Unity将会弹出一个选择项目位置的对话框。当你选择好存储位置后,Unity将会启动XCode运行项目,并且准备好了编译和运行项目了。
注意:不要仅仅在模拟器里跑你的游戏,因为Unity只提供了iOS的设备。在你的移动设备上去跑游戏。点这里查看跟多相关的内容。
运行成功之后,接下来就是为你的游戏制作一个简单的菜单了。
首先下载这个资源,里面包含了一些工具类来处理数据的存储和读取。解压压缩包,将里面的.cs文件拖到项目的Scripts文件夹中。
数据存储的这些类的实现并不在教程范围内,下面我们写一个小的测试来学习如何使用它。
在场景中创建一个空的GameObject,将LeadeboardController脚本拖上去。再在这个GameObject上添加一个脚本组件,命名为LeaderboardControllerTest,测试的内容是存储一些分数,然后再取回来。
你需要在测试类中索引LeaderboardController,添加一个LeaderboarfController的公有数据成员,代码如下:
using UnityEngine; using System.Collections.Generic; using System.Collections; public class LeaderboardControllerTest : MonoBehaviour { public LeaderboardController leaderboard; void Start () { } void Update () { } }
使用LeaderboardController中的AddPlayerScore方法来测试:
void Start () { leaderboard.AddPlayersScore( 100 ); leaderboard.AddPlayersScore( 200 ); }这样就可以将分数保存在闪存中,即使关掉应用,数据还是可以取回来。为了取回数据,需要注册LeaderboardControllers的OnScoresLoaded方法,同时还要实现对应的handler函数,代码如下.
顺便提一下 - 异步调用的方式可以允许你去扩展LeaderboardController,如果你想的话,让它能够处理一个远程的leaderboard。
void Start () { leaderboard.OnScoresLoaded += Handle_OnScoresLoaded; leaderboard.AddPlayersScore( 100 ); leaderboard.AddPlayersScore( 200 ); leaderboard.FetchScores(); } public void Handle_OnScoresLoaded( List<ScoreData> scores ){ foreach( ScoreData score in scores ){ Debug.Log ( score.points ); } }
在Handle_OnScoresLoaded方法中将会遍历每一个每个分数对象,然后将分数打印输出,这就是我们想要的。就是这样!
接下来要运行看看了:
创建一个新的GameObject,命名为LeaderboardControllder,将LeaderboardController.cs添加上去。
选定LeaderboardContrillerTest对象,将LeaderboardControllder赋给脚本中对应的公有成员。
点运行,看分数是不是在终端现实了!
是时候做点让人兴奋的新东西了 —— 你将会学到如何创建一个游戏菜单!
下面是我们将要做成的样子:
在Unity3D中实现用户界面有3种方式,每一种都各有优缺点,下面就仔细讨论一下。
1)GUI
Unity提供了一套自定义的用户界面,通过MonoBehaviour的回调函数OnGUI来处理事件,Unity支持用皮肤来改变这些界面的外观。
对于那些不是很在意性能的设备,这是一个很理想的解决方案,它提供了很丰富的预设控制,但是考虑到性能问题,自带的GUI不应该在游戏进行时出现。
2)GUITexture和GUIText
Unity提供了两个组件,GUITexture和GUIText,这两个组件可以让你能够在屏幕上呈现2D的图像和文字。你可以很方便地通过扩展这两个组件来创建自己的用户界面,相比于GUI 组件,性能上优秀很多。
3)3D Planes/Texture Altas
如果你要创建一个在游戏顶层现实的菜单(比如HUD),那么这个就是最好的选择。即使这个最麻烦!:] 但是一旦你创建好了顶层显示的相关类,你就很容易将它们适用在其他的新的项目中。
3D planes即用一套3D 平面来实现HUD,这些3D平面关联着同一个纹理集合,纹理集合就是将一些细小的纹理拼接在一起,组成一张大的纹理图片(译注:分辨率通常为2
的n次方,方面一次性加载到内存),这个和Cocos2D中的sprite sheet的概念类似!:]
由于各个HUD共享的是同一个材质(指向同一个纹理),通常只需要一个调用就可以将HUD全部渲染出来。在大部分情况下,你需要为HUD创建一个专用的摄像机,让它们看起来是正交投影的,而不是透视投影的(指的是摄像机的种类)。
在这个游戏中,我们的选择是第一种,Unity自带GUI。除了上面我们提到的它的一些缺点,它最大的好处就是有预置的控制,能够让这篇教程更简单一些。
下面我们首先为主菜单创建皮肤。然后你完成渲染主菜单的代码,最后将它链接到GameController上。
听起来是不是很棒!那就行动吧,少年!:]
Unity提供了一种叫Skin的东西来装饰GUI,这个东西可以简单的类比成Html的CSS。
我已经创建好了两个Skin(在第一部分的教程中已经导入到工程里面了),一个是480*320的分辨率,另一个是960*640的用于是视网膜屏幕的。下面的图片是480*320的Skin的属性。
Skin的属性文件有很多的选项,让你能够为你的项目创建独一无二的属性。在这个项目中,你只需要关心字体。
接下来打开GameMenuSmall,将scoreboard字体拖拽到Font属性并且将字体设置到16. 打开GameMenuNormal,将scoreboard字体拖拽到Font属性并且将字体设置到32.下一步就是制作真真的主菜单了!
编译运行
像之前做的一样,File->Build Settings.点击build按钮,开始测试你的第一个游戏吧!
编译并运行XCode项目,你就可以在你的设备上看到一个漂亮的并且可以work的菜单了。
主菜单
这个部分主要是GameMenuController的代码,负责渲染主菜单并且处理用户的输入。下面是代码中比较重要的片段,最终都会和游戏连接起来。
创建一个名为GameMenuController的脚本,创建下面的一些变量。
using UnityEngine; using System.Collections; [RequireComponent (typeof (LeaderboardController))] public class GameMenuController : MonoBehaviour { public Texture2D backgroundTex; public Texture2D playButtonTex; public Texture2D resumeButtonTex; public Texture2D restartButtonTex; public Texture2D titleTex; public Texture2D leaderboardBgTex; public Texture2D loginCopyTex; public Texture2D fbButtonTex; public Texture2D instructionsTex; public GUISkin gameMenuGUISkinForSmall; public GUISkin gameMenuGUISkinForNormal; public float fadeSpeed = 1.0f; private float _globalTintAlpha = 0.0f; private GameController _gameController; private LeaderboardController _leaderboardController; private List<ScoreData> _scores = null; public const float kDesignWidth = 960f; public const float kDesignHeight = 640f; private float _scale = 1.0f; private Vector2 _scaleOffset = Vector2.one; private bool _showInstructions = false; private int _gamesPlayedThisSession = 0; }
添加Awake()和Start()方法,如下:
void Awake(){ _gameController = GetComponent<GameController>(); _leaderboardController = GetComponent<LeaderboardController>(); } void Start(){ _scaleOffset.x = Screen.width / kDesignWidth; _scaleOffset.y = Screen.height / kDesignHeight; _scale = Mathf.Max( _scaleOffset.x, _scaleOffset.y ); _leaderboardController.OnScoresLoaded += HandleLeaderboardControllerOnScoresLoaded; _leaderboardController.FetchScores(); }
代码中scale offsets用来保证GUI元素能够正常地缩放。比如,如果一个菜单是960*640的,当前设备的分辨率是480*320,然后你需要做的就是将菜单缩小50%,那么scaleOffset就是0.5. 这么做在简单的多分辨率设备的适配中会很不错,你不需要重复创建资源。
一旦scores加载完毕,在本地存储起来,将会用来渲染GUI.
public void HandleLeaderboardControllerOnScoresLoaded( List<ScoreData> scores ){ _scores = scores; }
让我们稍微休息一下,测试测试目前为止我们所做的东西。
在GameMenuController中添加下面的code:
void OnGUI () { GUI.DrawTexture( new Rect( 0, 0, Screen.width, Screen.height ), backgroundTex ); if (GUI.Button( new Rect ( 77 * _scaleOffset.x, 345 * _scaleOffset.y, 130 * _scale, 130 * _scale ), resumeButtonTex, GUIStyle.none) ){ Debug.Log( "Click" ); } }
第一句话是用一张纹理绘制整个屏幕。
GUI.DrawTexture( new Rect( 0, 0, Screen.width, Screen.height ), backgroundTex );
if (GUI.Button( new Rect ( 77 * _scaleOffset.x, 345 * _scaleOffset.y, 130 * _scale, 130 * _scale ), resumeButtonTex, GUIStyle.none) ){ Debug.Log( "Click" ); }
为了测试,将GameMenuController脚本附加到GameController上,将对应的公有变量拖拽上去,如下图:
现在测试吧,点击运行,你会开发出现了一个button,点击它你就可以在终端看到打印的信息。
还不赖吧?完成菜单的第一步就搞定了!:]
现在你确定了你的方向找对了,接下来根据屏幕的尺寸来设置skin。用下面的代码来替换OnGUI函数:
if( _scale < 1 ){ GUI.skin = gameMenuGUISkinForSmall; } else{ GUI.skin = gameMenuGUISkinForNormal; }
显示和隐藏
相比于粗鲁地弹出菜单,用淡入淡出来处理是更好的选择。为了实现淡入淡出,我们需要处理GUI的static 变量 content Color (这回影响到GUI类里绘制的所有内容);
为了处理淡入,你应该慢慢地将_globalTintAlpha的值从0增加到1.然后将它赋给GUI.contenColor变量。将西面的代码加入OnGUI函数:
_globalTintAlpha = Mathf.Min( 1.0f, Mathf.Lerp( _globalTintAlpha, 1.0f, Time.deltaTime * fadeSpeed ) ); Color c = GUI.contentColor; c.a = _globalTintAlpha; GUI.contentColor = c;
public void Show(){ // ignore if you are already enabled if( this.enabled ){ return; } _globalTintAlpha = 0.0f; _leaderboardController.FetchScores(); this.enabled = true; } public void Hide(){ this.enabled = false; }
菜单显示的内容根据游戏的状态不同,内容也不同,比如当游戏结束的时候菜单的内容就和暂停时显示的菜单不一样。
在OnGUI函数中添加下面的代码:
GUI.DrawTexture( new Rect( 0, 0, Screen.width, Screen.height ), backgroundTex ); if( _gameController.State == GameController.GameStateEnum.Paused ){ if (GUI.Button( new Rect ( 77 * _scaleOffset.x, 345 * _scaleOffset.y, 130 * _scale, 130 * _scale ), resumeButtonTex, GUIStyle.none) ){ _gameController.ResumeGame(); } if (GUI.Button( new Rect ( 229 * _scaleOffset.x, 357 * _scaleOffset.y, 100 * _scale, 100 * _scale ), restartButtonTex, GUIStyle.none) ){ _gameController.StartNewGame(); } } else{ if (GUI.Button( new Rect ( 77 * _scaleOffset.x, 345 * _scaleOffset.y, 130 * _scale, 130 * _scale ), playButtonTex, GUIStyle.none) ) { if( _showInstructions || _gamesPlayedThisSession > 0 ){ _showInstructions = false; _gamesPlayedThisSession++; _gameController.StartNewGame(); } else{ _showInstructions = true; } } }
你应该很熟悉这些代码,就是根据游戏的状态渲染对应的纹理和按钮。暂停状态下的两个按钮分别允许玩家返回和重新开始:
if (GUI.Button( new Rect ( 77 * _scaleOffset.x, 345 * _scaleOffset.y, 130 * _scale, 130 * _scale ), resumeButtonTex, GUIStyle.none) ){ _gameController.ResumeGame(); } if (GUI.Button( new Rect ( 229 * _scaleOffset.x, 357 * _scaleOffset.y, 100 * _scale, 100 * _scale ), restartButtonTex, GUIStyle.none) ){ _gameController.StartNewGame(); }
注:想知道我是怎么知道那些精确的尺寸的?答案是使用GIMP!
另一个状态是GameOver,这个时候你需要渲染开始按钮。
注:你可能注意到了两个变量 _showInstructions 和 _gamesPlayerdThisSession 。_gamesPlayerThisSession是用来记录当前你玩了多少场游戏的,如果是一次,那么 _showInstructions 就为真,那么我们就可以在玩家开始玩游戏之前给出一些游戏提示。
(译注:这两个变量都作为GameMenuController的私有成员)
在完成GameMenuController之前,确保现在的每一个功能都如预期那样工作。一切都设置好了之后,你就可以看到和下面类似的画面了:
完成GameMenuController
最后要完成的是标题,提示还有分数。
绘制标题还是提示是根据上面提到的 _showInstructions 标志位;将下面的代码添加在OnGUI方法的最下面:
if( _showInstructions ){ GUI.DrawTexture( new Rect( 67 * _scaleOffset.x, 80 * _scaleOffset.y, 510 * _scale, 309 * _scale ), instructionsTex ); } else{ GUI.DrawTexture( new Rect( 67 * _scaleOffset.x, 188 * _scaleOffset.y, 447 * _scale, 113 * _scale ), titleTex ); }
简单的按钮,用于关联facebook和Twitter,然后将所有分数一个个加起来。将下面的代码添加在OnGUI方法的最下面:
GUI.BeginGroup( new Rect( Screen.width - (214 + 10) * _scale, (Screen.height - (603 * _scale)) / 2, 215 * _scale, 603 * _scale ) ); GUI.DrawTexture( new Rect( 0, 0, 215 * _scale, 603 * _scale ), leaderboardBgTex ); Rect leaderboardTable = new Rect( 17 * _scaleOffset.x, 50 * _scaleOffset.y, 180 * _scale, 534 * _scale ); if( _leaderboardController.IsFacebookAvailable && !_leaderboardController.IsLoggedIn ){ leaderboardTable = new Rect( 17 * _scaleOffset.x, 50 * _scaleOffset.y, 180 * _scale, 410 * _scale ); GUI.DrawTexture( new Rect( 29* _scaleOffset.x, 477* _scaleOffset.y, 156 * _scale, 42 * _scale ), loginCopyTex ); if (GUI.Button( new Rect ( 41 * _scaleOffset.x, 529 * _scaleOffset.y, 135 * _scale, 50 * _scale ), fbButtonTex, GUIStyle.none) ) { _leaderboardController.LoginToFacebook(); } } GUI.BeginGroup( leaderboardTable ); if( _scores != null ){ for( int i=0; i<_scores.Count; i++ ){ Rect nameRect = new Rect( 5 * _scaleOffset.x, (20 * _scaleOffset.y) + i * 35 * _scale, 109 * _scale, 35 * _scale ); Rect scoreRect = new Rect( 139 * _scaleOffset.x, (20 * _scaleOffset.y) + i * 35 * _scale, 52 * _scale, 35 * _scale ); GUI.Label( nameRect, _scores[i].name ); GUI.Label( scoreRect, _scores[i].points.ToString() ); } } GUI.EndGroup(); GUI.EndGroup(); }
声明几个变量:
private GameMenuController _menuController; private LeaderboardController _leaderboardController; public Alerter alerter;
void Awake() { _instance = this; _menuController = GetComponent<GameMenuController>(); _leaderboardController = GetComponent<LeaderboardController>(); }
public GameStateEnum State{ get{ return _state; } set{ _state = value; // MENU if( _state == GameStateEnum.Menu ){ player.State = Player.PlayerStateEnum.BouncingBall; _menuController.Show(); } // PAUSED else if( _state == GameStateEnum.Paused ){ Time.timeScale = 0.0f; _menuController.Show(); } // PLAY else if( _state == GameStateEnum.Play ){ Time.timeScale = 1.0f; _menuController.Hide(); // notify user alerter.Show( "GAME ON", 0.2f, 2.0f ); } // GAME OVER else if( _state == GameStateEnum.GameOver ){ // add score if( _gamePoints > 0 ){ _leaderboardController.AddPlayersScore( _gamePoints ); } // notify user alerter.Show( "GAME OVER", 0.2f, 2.0f ); } } }
最后,注意这段代码有依赖于一个Alerter对象。所以为了让它跑起来,创建一个空对象,将Alert脚本添加上去,然后将Alerter拖拽到GameController的对应属性上。
就像之前的一样,File->Build Setting打开编译对话框,点击编译按钮来测试最终的游戏!
编译并运行XCode项目,你将会在你的设备上看到一个美腻的菜单!
优化方面的内容可以写成一本书了!即使你觉得游戏的表现还能接受,你有考虑过那一大堆ipod touch和iPhone 3G的感受么?你已经花了很大的力气去完成一个游戏,你应该不愿意那些持有老设备的玩家觉得你的游戏很卡吧!
下面的一些条款在开发的时候最好牢记在心:
最小化调用绘制函数 —— 尽可能少地调用绘制函数,你可以共享纹理和材质,避免使用透明的shader - 使用mobile shader来替代。限制场景的光源数量,使用贴图组合来实现HUD.
注意有些场景的复杂度 —— 使用优化的模型,意味着模型具有更少的图元。你通常可以将模型的细节烘培到纹理之中,而不是使用高精度的模型,烘培对于光照同样适用。记住玩家玩的游戏是在很小的一个屏幕上,很多细节都会被忽略。
适用假的阴影 —— 动态阴影在iOS中并不能适用,但是可以使用projectors来做成一个假的阴影。唯一可能引起的问题就是projector会调用绘制函数,所以如果可能的话,尽可能用一个带阴影纹理的平面加上一个特粒子Shader来模拟。
警惕在Update/FixexUpdate方法中的任何代码 —— 理想情况下,Update和FixedUpdate函数需要每秒跑30到60次,所以在调用这两个函数之前,预处理好任何可以做的事情。
同时也要注意你加在其中的逻辑,特别是和物理相关的!
关闭所有不使用的东西 —— 如果一个脚本不需要执行,就关掉它,虽然它看起来没那么重要 —— app中所有的一切都会消耗CPU!
尽可能使用最简单的组件 —— 如果你只需要一个组件中很小的一部分功能,其他大部分都用不着,那么你可以自己实现你最需要的那部分功能而不是直接拿来用。比如CharacterController就是一个很诱人的组件,所以最好用Rigidbody来实现你自己的解决方案。
整个开发过程都使用真机来测试 —— 当运行游戏的时候,打开console的debug log窗口,那样就可以看你的app消耗了多少cpu。你需要这样做:在XCode中找到iPhone_Profiler.h文件,并且将ENABLE_INTERNAL_PROFILER 设为1.这样就可以更加详细地看到你的app的运行情况。若果你有Unity3D Advance版本,里面有个profiler能够查看脚本中每个方法消耗的时间。profiler的信息就像下面这样:
帧率可以表示游戏运行的速度,默认的速度哦要么是30,要么60.游戏的平均帧率应该接近这两个值。
draw-call表示当前渲染调用的次数 - 就像前面提到的,通过共享纹理和材质,将这个数字保持得越低越好。
(译注:一个 Draw Call,等于呼叫一次 DrawIndexedPrimitive (DX) or glDrawElements (OGL),等于一个 Batch。尽可能地减少Drawcall的数量。IOS设备上建议不超过100。减少的方法主要有如下几种:Frustum Culling,Occlusion Culling,Texture Packing。Frustum Culling是Unity内建的,我们需要做的就是寻求一个合适的远裁剪平面;Occlusion Culling,遮挡剔除,Unity内嵌了Umbra,一个非常好OC库。但Occlusion Culling也并不是放之四海而皆准的,有时候进行OC反而比不进行还要慢,建议在OC之前先确定自己的场景是否适合利用OC来优化;Texture Packing,或者叫Texture Atlasing,是将同种shader的纹理进行拼合,根据Unity的static batching的特性来减少draw call。建议使用,但也有弊端,那就是一定要将场景中距离相近的实体纹理进行拼合,否则,拼合后很可能会增加每帧渲染所需的纹理大小,加大内存带宽的负担。这也就是为什麽会出现“DrawCall降了,渲染速度也变慢了”的原因)
verts表示当前需要渲染多少顶点。
player-detail可以告诉我们游戏引擎的每个部分消耗了多少时间。
你可以下载完整的项目,然后再unity中打开。(译注:有点坑爹,很多bug)
到现在为止,你已经做得很好了,但我们的旅程远不会到此为止!:] 保持这股势头,你将会成为一个unity高手,当然,这也需要更多的各方面的技能。
下面是一些扩展游戏的建议:
● 添加音效。声音是交互内容的非常重要的一个内容,所以花些时间去找些音乐和音效加到游戏中去吧。
● 关联Facebook。
● 增加多玩家模式,让玩家们能够同一个设备上竞技,轮流投球。
● 增加角色让玩家选择;
● 支持多种手势,不同的手势代表不同的投篮方式;
● 添加带奖励的篮球,这样的篮球有不一样的物理属性,比如不同的重量;
● 添加新的关卡,每个关卡有新的挑战!
这些足够让你忙一阵了!
希望你喜欢这个系列的教程,并且能够学习一些unity的只是。我希望看到你们将来制作出属于自己的unity app!
Project part3
Data Control Resources
Intermediate Unity 3D for iOS: Part 3/3 - http://www.raywenderlich.com/20420/beginning-unity-3d-for-ios-part-3
原工程的代码导入到unity3d4.3无法正常运行,个人重新编写了一个android和pc上都能运行的完整版本,点我下载。