Intermediate Unity 3D for iOS: Part 3/3

来源:互联网 发布:面试谈谈工作经历知乎 编辑:程序博客网 时间:2024/06/06 10:02

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 the first part of the series, you learned how to use the Unity interface to lay out a game scene, and set up physics collisions.

In the second part of the series, you learned how to script with Unity, and created most of the game logic, including shooting and scoring baskets!

In this part and final of the tutorial, you’ll be working to build a menu system for the user and interacting with it via your GameController. Let’s get started!

Testing on your Device

So far, you have been testing with the built-in Unity simulator. But now that you have your project in a functional state it’s about time to run this on an actual device for testing!

To do this, open the Build dialog via File -> Build Settings. You’ll then be presented with the following dialog:

First off, ensure you have the right platform selected (iOS should have the Unity icon next to it as shown above, if not then highlight the iOS and click on the ‘Switch Platform’).

Select “Player Settings” to bring up the build setting in the inspector panel.

Unity3D build properties

There is a multitude of settings to select from, but (for now) you really only need to concern yourself with ensuring that the game is oriented correctly.

In the ‘Resolution and Presentation’ pane select Landscape-Left from the dropbox for the devices orientation and leave the rest as their defaults and move onto the next pane ‘Other Settings’.

In this section you’ll need to enter in your developer Bundle Identifier (this has the equivalent when developing in XCode) and leave the rest with their default values.

The Moment of Truth

Once you’ve set the appropriate values, go back to the Build Settings dialog and select Build.

Unity will prompt you for a destination for your project. Once you’ve selected the destination, Unity will launch XCode with your project ready to be built and run on your device.

Note: Don’t try running your game in a simulator as the Unity libraries are only for iOS devices. Running Unity projects on your device has all the usual requirements as far as certificates, App IDs and provisioning profiles go. Check out this thread on the Unity Answers site for more details.

The game running on an iPhone device!

Once you finish playing around come back and you’ll continue with building a simple menu for your game.

Keeping Score

First download the resources for this tutorial, which include some support classes that handle the persisting and retrieving of scores. Extract the zip and drag the .cs files into your Scripts folder in Unity.

The implementation of this is out of scope for this tutorial but as you will be using it you will run through a quick testable example of how it is used.

Start by creating a new empty GameObject on your scene and attach the LeaderboardController (included with this tutorials packages) script to it.

Create a new Script named LeaderboardControllerTest and attach it to a newly created GameObject. You will perform a simple test where you store a couple of scores and then retrieve them back.

To do this you need reference to the LeaderboardController so start off by adding a public LeaderboardController variable to your LeaderBoardControllerTest class, as shown below:

using UnityEngine;using System.Collections.Generic;using System.Collections; public class LeaderboardControllerTest : MonoBehaviour {public LeaderboardController leaderboard;  void Start () {} void Update () {}}

Note: Take note of the using System.Collections.Generic; at the top of the class, this is asking that the classes belonging to the System.Collections.Generic package are included so you can reference the Generic specific collections. Explanation of Generic can be found here.

Use the AddPlayersScore method of the LeaderboardController to add the player score (who would have thought):

void Start () {leaderboard.AddPlayersScore( 100 ); leaderboard.AddPlayersScore( 200 ); }

This will persist the score to disk that you can retrieve even after you have closed the application. To retrieve you need to register for the LeaderboardControllers OnScoresLoaded event, along with the implemented handler method and finally request for the scores, as shown below.

By the way – the reason for the asynchronous call is to allow you to extend the LeaderboardController to handle a remote leaderboard later if you want.

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 );}}

The parameter List is a list of ScoreData objects, the ScoreData is a simple data object that encapsulates the details of the score (a record).

The Handle_OnScoresLoaded method will iterate through all the scores and output their points, just what you need.

That’s it! Now test it out by doing the following:

  • Create a new GameObject, name it LeaderboardController, and attach the LeaderboardController script to it.
  • Select the LeaderboardControllerTest object, and attach the LeaderboardController object to the leaderboard property.
  • Click Play, and you should see the scores log out to the console!

Creating a simple menu

Now for something new and exciting – you’re going to learn how to create a menu for the game!

Here’s a screenshot of what you’re aiming to build:

There are three paths you can choose when implementing a user interface in Unity. Each has its advantages and disadvantages. The following section explains each in detail.

1) GUI

Unity provides a set of pre-defined user interface controls that can be implemented using the GUI Component via the MonoBehaviour hook method OnGUI. Unity also supports the ability to customize the appearance of the menu using Skins.

For scenes that aren’t performance-critical, this is an ideal solution as it provides the richest set of premade controls. However, as it has performance concerns, it should not be used during game play!

2) GUITexture and GUIText

Two components Unity provides are the GUITexture and GUIText. These components allow you to present flat (2D) images and text on the screen. You can easily extend these to create your user interface with a reduced hit on performance compared to using the GUI Component controls.

3) 3D Planes / Texture Altas

If you’re creating a heads up display (HUD i.e. a menu shown during game-play) then this is the preferred option; even though it requires the most effort! :] But once you have built the supporting classes for your heads up display, you can port them to every new project.

3D planes refers to implementing the HUD using a combination of flat 3D planes associated with a texture atlas, a texture atlas being a collection of many discrete images that has been saved as one large image. It’s similar in concept to a sprite sheet for all you Cocos2D users! :]

Since the Material (which references the texture) is shared across all of your HUD elements, usually only one call is required to render the HUD to the screen. In most cases, you would use a dedicated Camera for the HUD as its likely you’ll be rendering them in orthographic projection rather than perspective (which is a mode of the camera).

The option you will use in this tutorial is #1 – Unity’s GUI. Despite the above recommendations to avoid using it, it does have a host of pre-built controls which will keep this tutorial manageable! :]

You will start by creating the Skins for the main menu. Then you’ll work through the code which renders the main menu, and finally tie it all together by linking it with the GameController.

Sound good? Time to get started with skinning! :]

Skins

Unity provides a way to dress up the GUI elements using something called a Skin. This can be thought of as a simple stylesheet used in conjunction with HTML elements.

I created two skin files for you (which you already imported into your project way back in the first part of this tutorial), one for 480×320 displays, and the other for the 960×640 Retina display. The following is a screenshot of the properties of the 480×320 skin:

The Skin property file exposes a lot of attributes that allow you to create unique styles for your project. For this project, you only need to be concerned with the font.

So open the GameMenuSmall skin and drag the scoreboard font onto the Font property and set the Font size to 16. Then open the GameMenuNormal and drag the scoreboard font onto the Font property and set the Font size to 32.

Next up is implementing the actual main menu!

Main Menu

This section presents the code for the GameMenuController which is responsible for rendering the main menu and interpreting user input. You’ll work quickly through the important parts of the code, and then finally hook it up to your game!

Create a new script named GameMenuController and add the following variables as shown below:

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; }

First, there is a set of publicly accessible variables, which are assigned within the editor that gives theGameMenuController access to elements used to render the main menu. Next you have variables to reference the two skins you created in the previous section.

Following that you have variables that you’ll use to fade in and out the main menu.

We also include references of the GameController and LeaderboardController with reference to the scores you retrieve back from your LeaderboardController.

Following this you have a set of constants and variables which are used to determine the scale of the user interface elements i.e. to manage the different screen resolutions of the, for example, iPhone 3GS (480×320) and iPhone 4 (960×640).

And finally you have some variables you use to manage the state of the GameMenuController Component.

Next add the Awake() and Start() methods, as shown below:

        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(); }

During Start(), the scores are requested from the LeaderboardController. As well, some graphics ratios are calculated so that the GUI can be adjusted accordingly (as mentioned above).

The scale offsets in the code above are used to ensure the GUI elements are scaled appropriately. For instance, if the menu is designed for 960×640, and the current device resolution is 480×320, then you need to scale these down by 50%; your scaleOffset will be 0.5. This works fairly well if you’re using simple graphics without the need of duplication, and will become more relevant when you start porting to devices with multiple resolutions.

Once the scores are loaded, cache the scores locally (this should look familiar to you as we’ve just implemented something very similar in the above section), which will be used when rendering the GUI:

public void HandleLeaderboardControllerOnScoresLoaded( List<ScoreData> scores ){_scores = scores; }

Time to test

Lets take a little break and test what you have so far.

Add the following code to your GameMenuController:

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" );  }}

The above snippet of code shows the OnGUI method. This method is called in a manner similar to Update(), and provides access to the GUI Component. The GUI Component provides a suite of static methods exposing standard user interface controls, click here to learn more about the OnGUI and GUI class from the official Unity site.

Within the OnGUI method you are asking a texture to be drawn across the whole screen with the following code:

GUI.DrawTexture( new Rect( 0, 0, Screen.width, Screen.height ), backgroundTex );

Next you wrap the GUI.Button method with a conditional statement, the GUI.Button method renders a button at the specified location (using the offsets you calculated before to handle differing screen resolutions). This method also returns a boolean whether the user has clicked on it or not i.e. will return true if the user has clicked on it otherwise false.

if (GUI.Button( new Rect ( 77 * _scaleOffset.x, 345 * _scaleOffset.y, 130 * _scale, 130 * _scale ), resumeButtonTex, GUIStyle.none) ){Debug.Log( "Click" );  }

In this case if it has been clicked then it will output “Click” to the console.

To test, attach your GameMenuController script to the GameObject your GameController is attached to, and then set attach all the public properties to the appropriate objects, as shown below:

Now try it out! Click play and you’ll see a button appear. Click on it and you’ll see a message in the console!

Not bad, eh? The first small step of your menu is underway! :]

Using your skins

Now that you’ve confirmed you’re heading in the right direction, let’s continue by assigning the appropriate skin depending on the screen size.

Replace the body of your OnGUI method with the following code:

if( _scale < 1 ){GUI.skin = gameMenuGUISkinForSmall; } else{GUI.skin = gameMenuGUISkinForNormal;}

The skins will ensure that you use the correct font size (based on the screensize); you determine what skin to use based on the _scale you calculated previously. If it is less than 1.0 then you will use the small skin otherwise revert to the normal skin.

Showing and hiding

Rather than abruptly popping up the GUI when requested, you will gradually fade in. To do this you will manipulate the GUI’s static contentColor variable (this affects all subsequent drawing done by the GUI class).

To manage the fading in you will granularly increase your _globalTintAlpha variable from 0 to 1 and assign this to your GUI.contentColor variable.

Add the following code to your OnGUI method:

_globalTintAlpha = Mathf.Min( 1.0f, Mathf.Lerp( _globalTintAlpha, 1.0f, Time.deltaTime * fadeSpeed ) );  Color c = GUI.contentColor;c.a = _globalTintAlpha; GUI.contentColor = c;

You also need a way to initiate the showing and hiding of your menu, so create two publicly accessible method called Show and Hide (as shown below):

public void Show(){// ignore if you are already enabledif( this.enabled ){return; }_globalTintAlpha = 0.0f; _leaderboardController.FetchScores(); this.enabled = true; } public void Hide(){this.enabled = false; }

Nothing fancy going on here! You request a new batch of scores in case the user has added a new score. Then you adjust the global tint alpha to 0 and enable/disable this Component to start/stop the OnGUI call (i.e. if this Component is disabled then all update methods e.g. Update, FixedUpdate, OnGUI wont be called).

What your menu displays will depend on what state the game is in e.g. Pause will render differently to GameOver.

Add the following code to your OnGUI method:

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; }}}

This should look fairly familiar to you; all you are doing is rendering textures and buttons depending on what state the GameController is in.

When paused you render two buttons to allow the user to resume or restart:

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(); }

Note: Wondering how I got those position and size numbers? I painfully copied them across from GIMP, which I used to create the interface.

The other state it could be in is GameOver, which is when you render the play button.

Note: You may have noticed the two variables _showInstructions and _gamesPlayedThisSession. The _gamesPlayedThisSession is used to determine how many games you have played for this session, if it is the first game then you flag _showInstructions to true before playing the game. This allows use to display a set of instructions (shown next) before the user plays their first game of Nothing But Net.

Time to test

Before you finish off the GameMenuController, lets make sure everything is working at expected. Everything should be setup from your previous test so you should be able to press the play button and see something similar to the following:

Finishing off the GameMenuController

The final pieces left is the title, instructions, and score.

Drawing the title or instruction is dependent on the instruction flag (as described above); add the following code to the bottom of your OnGUI method:

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 );}

That take care of that; the final piece is the scoreboard. OnGUI provides groups, groups allow you to group things together in a specified layout (horizontal or vertical). The following code draws the leaderboard and some dummy buttons for Facebook / Twitter and then iterates through all the scores adding them individually. Add the following code to the bottom of your OnGUI method:

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(); }

And that’s that for your GameMenuController; to finish off lets hook up your GameMenuController to your GameController class (which tells it to show and hide as required), open up the GameController and lets make your way through it.

So open up GameController and declare the following variables at the top:

private GameMenuController _menuController;private LeaderboardController _leaderboardController;public Alerter alerter;

Now get reference to them in the Awake method;

void Awake() {_instance = this; _menuController = GetComponent<GameMenuController>();_leaderboardController = GetComponent<LeaderboardController>();}

The most significant change is within your State property; replace your UpdateStatePlay with the following and afterwards we’ll summarize the changes:

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 useralerter.Show( "GAME ON", 0.2f, 2.0f ); } // GAME OVER else if( _state == GameStateEnum.GameOver ){// add score if( _gamePoints > 0 ){_leaderboardController.AddPlayersScore( _gamePoints ); } // notify useralerter.Show( "GAME OVER", 0.2f, 2.0f ); }}}

The code should hopefully be pretty self-explanatory; when the state is updated to Menu or Pause you ask your GameMenuController to show itself using the Show method you implemented. If the state is set to Play then you ask the GameMenuController to hide itself using the Hide method. And finally when the state is changed to GameOver you add the players score to your leaderboard (just how you did it when you created your example).

Finally, notice that this code depends on an Alerter object. So to make this work, create a new empty object, assign it the Alerter script, then drag that object onto the Alerter property on the Game Controller.

Build and Run

As you did before, open the Build dialog via File -> Build Settings, and click on the build button to test your finished game!

Build and run the Xcode project, and you should see a beautiful working menu on your device!

Finished game on the iPhone.

w00t you’ve got a complete simple Unity 3D game! :]

Optimizations

A book could be written about optimizing your app! Even if you think the performance is acceptable, consider that there are a LOT of old iPod touches and iPhone 3G models out there. You’ve worked hard to make a game, and you don’t people with older devices to think your games are laggy!

Here’s a list of items to keep in mind when developing:

  • Keep draw calls to a minimum — You should aim to keep your draw calls as minimal as possible. To achieve this, share textures and materials, and avoid using transparent shaders — use mobile shaders instead. Limit the number of lights you use, and always use a texture atlas for your HUD.
  • Keep an eye on the complexity of your scene — Use optimized models, meaning models with minimal geometry. Instead of using geometry for the details, you can usually achieve the same effect by baking the details into the textures, similar to how you would bake in lighting. Remember that the user is only staring at a small screen, so a lot of detail won’t be picked up.
  • Fake the shadows with the model — Dynamic shadows are not available in iOS, but projectors can be used to mimic shadows. The only issue is that projectors drive up your draw calls, so if possible, use a flat plane with a shadow texture that uses a a particle Shader to mimic the shadow.
  • Be wary of anything you place in your Update/FixedUpdate methods — Ideally the Update() and FixedUpdate() calls run 30 to 60 times per second. Because of this, ensure that you calculate or reference everything possible before these are called. Also be wary of any logic you add to these modules, especially if they are physics related!
  • Turn off anything you’re not using — If you don’t need a script to run, then disable it. It doesn’t matter how simple it appears to be – everything in your app consumes processor time!
  • Use the simplest component possible — If you don’t require most of the functionality of a component, then write the parts you need yourself and avoid using the component altogether. As an example, CharacterController is a greedy component, so it’s best to roll your own solution using Rigidbody.
  • Test on the device throughout development — When running your game, turn on the console debug log so you can see what may be eating up processor cycles. To do this, locate and open the iPhone_Profiler.h file in XCode and set ENABLE_INTERNAL_PROFILER to 1. This gives you a high-level view of how well your app is performing. If you have Unity 3D Advance, then you can take advantage of the profiler which will allow you to drill down into your scripts to find how much time each method is consuming. The output of the internal profiler looks something like the following:

  • frametime gives you an indication of how fast your game loop is; by default this is set to either 30 or 60. Your average should be hitting somewhere near this value.
  • draw-call gives you the number of draw calls for the current render call – as mentioned before, keep this as low as possible by sharing textures and materials.
  • verts gives you an snapshot of how many vertices are currently being rendered
  • player-detail gives you a nice overview of how much time each component of your game engine is consuming

Where To Go From Here?

Here is a sample project with the complete finished game from this tutorial series. 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.

You’ve done well to get this far, but the journey doesn’t stop here! :] Hopefully you will keep up the momentum and become a Unity ninja; there is definitely a lot of demand for these types of skills at the moment!

Here are a few suggestions of how you can extend your game:

  • Add sound. Sound is extremely important for any interactive content, so spend some time looking into sound and music and add it to the game.
  • Hook up Facebook so users can compete with friends.
  • Add multiplayer mode, which will allow users to play against each other on the same device, where each player takes turns throwing.
  • Add new characters to allow the user to personalize the game.
  • Extend the user input to allow different gestures, and then use these gestures to implement different types of throws.
  • Add bonus balls, with each ball having different physical attributes, such as increased gravity.
  • Add new levels, with each level being ever more challenging!

That should be enough to keep you busy! :]

I hope you enjoyed this tutorial series and learned a bit about Unity. I hope to see some of you create some Unity apps in the future!

If you have any comments or questions on this tutorial or Unity in general, please join the forum discussion below!


This is a tutorial by Joshua Newnham, the founder of We Make Play, an independent studio crafting creative digital play for emerging platforms.



From: http://www.raywenderlich.com/20420/beginning-unity-3d-for-ios-part-3


0 0
原创粉丝点击