«

Intermediate Unity 3D for iOS: Part 2/3

时间:2024-3-2 19:35     作者:韩俊     分类: Android


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
Assets
Materials and Textures
Scene Positioning
Lighting
Camera Positioning
Physics and Colliders
Prefabs

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 FileOpen Project, click Open Other, and browse to the folder. Note that the scene won’t load by default – to open it, select ScenesGameScene.

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 theGameController 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 scripting languages; 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:

using UnityEngine;
using System.Collections;

public class DummyScript : MonoBehaviour {

    // Use this for initialization
    void Start () {

    }

    // Update is called once per frame
    void Update () {

    }
}


The Start() and Update() methods above are known as hook methods; 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”.

Note: MonoBehaviour has another update method called FixedUpdate(). This method is called by the physics engine and should only be used to update Rigidbody or other physics-based properties. It’s called FixedUpdate() as it’s guaranteed to be called at fixed intervals, unlike the Update() method which is called every “tick”, where the amount of time between ticks can be variable.

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 atMonoDevelop.

Insert the code below into the new script:

using UnityEngine;
using System.Collections;

public class ScoreBoard : MonoBehaviour
{        

    public TextMesh pointsTextMesh;
    public TextMesh timeRemainingTextMesh;

    void Awake ()
    {       
    }

    void Start ()
    { 
    }

    void Update ()
    {    
    }

    public void SetTime (string timeRemaining)
    {
        timeRemainingTextMesh.text = timeRemaining;     
    }

    public void SetPoints (string points)
    {
        pointsTextMesh.text = points;   
    }   
}


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:

using UnityEngine;
using System.Collections;

public class ScoreBoardTest : MonoBehaviour
{        

    public ScoreBoard scoreboard; 

    public void Start()
    {
        scoreboard.SetTime( "60" );
        scoreboard.SetPoints( "100" );
    }

}


Next click GameObjectCreate 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:

using UnityEngine;
using System.Collections;

[RequireComponent (typeof(SphereCollider))]
[RequireComponent (typeof(Rigidbody))]
public class Ball : MonoBehaviour
{

    private Transform _transform;
    private Rigidbody _rigidbody;
    private SphereCollider _sphereCollider; 

    public delegate void Net ();

    public Net OnNet = null;
    private GameController _gameController;

    void Awake ()
    {
        _transform = GetComponent<Transform>();
        _rigidbody = GetComponent<Rigidbody>();
        _sphereCollider = GetComponent<SphereCollider>();
    }

    void Start ()
    {         
        _gameController = GameController.SharedInstance;
    }

    void Update ()
    {

    }

    public Transform BallTransform {
        get {
            return _transform; 
        }   
    }

    public Rigidbody BallRigidbody {
        get {
            return _rigidbody;  
        }
    }

    public SphereCollider BallCollider {
        get {
            return _sphereCollider;     
        }
    }

    public void OnCollisionEnter (Collision collision)
    {
        _gameController.OnBallCollisionEnter (collision);
    }

    public void OnTriggerEnter (Collider collider)
    {
        if (collider.transform.name.Equals ("LeftHoop_001")) {
            if (OnNet != null) {
                OnNet ();    
            }   
        }
    }
}


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:

[RequireComponent (typeof (SphereCollider))]
[RequireComponent (typeof (Rigidbody))]


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.

private Transform _transform;
private Rigidbody _rigidbody;
private SphereCollider _sphereCollider; 

void Awake ()
{
    _transform = GetComponent<Transform>();
    _rigidbody = GetComponent<Rigidbody>();
    _sphereCollider = GetComponent<SphereCollider>();
}


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).

private GameController _gameController; 

void Start () {         
    _gameController = GameController.SharedInstance; 
}


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.

public delegate void Net();
public Net OnNet = null;


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.

public void OnCollisionEnter( Collision collision ){
    _gameController.OnBallCollisionEnter( collision );
}


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. :]

Note: along with OnCollisionEnter(), there are also OnCollisionStay() and OnCollisionExit() methods, which are called each frame as an object is colliding with something else, and when it stops colliding, respectively. For more details, check out the official Unity documentation athttp://docs.unity3d.com/Documentation/ScriptReference/MonoBehaviour.html.

public void OnTriggerEnter( Collider collider ) {
    if( collider.transform.name.Equals ( "LeftHoop_001" ) ) {
        if( OnNet != null ){
            OnNet();    
        }   
    }       
}


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:

using UnityEngine;
using System.Collections;

public class GameController : MonoBehaviour {

    private static GameController _instance = null;

    public static GameController SharedInstance {
        get {
            if (_instance == null) {
                _instance = GameObject.FindObjectOfType (typeof(GameController)) as GameController;
            }

            return _instance;
        }
    }

    void Awake() {      
        _instance = this; 
    }

    public void OnBallCollisionEnter (Collision collision) {
        Debug.Log ( "Game Controller: Ball collision occurred!" );    
    }

}


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.

Note: if you’re curious about using the Singleton pattern in other iOS projects, a discussion of implementing singletons in iOS 4.1 and above can be found on Stack Overflow.

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.

Note: When implementing a Singleton you would normally set its constructor to private such that the Singleton accessor controlled the instantiation. Since we are inheriting from a Unity MonoBehaviour class we are unable to make the constructor private. So it’s an implied Singleton and must be enforced explicitly by the programmer.

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:

using UnityEngine;

public class BallTest : MonoBehaviour {
    public Ball ball; 
    void Start () {
        ball.OnNet += Handle_OnNet; 
    }

    protected void Handle_OnNet(){
        Debug.Log ( "NOTHING BUT NET!!!" );   
    }
}


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 (WindowConsole), 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 theGameController has been completed.

Create a new script named Player and edit it in MonoDevelop as follows:

using UnityEngine;
using UnityEngine;
using System.Collections;

[RequireComponent (typeof(Animation))]
public class Player : MonoBehaviour
{

    public delegate void PlayerAnimationFinished (string animation);

    public PlayerAnimationFinished OnPlayerAnimationFinished = null;    
    private Vector3 _shotPosition = Vector3.zero; 
    public Vector3 ShotPosition{
        get{
            return _shotPosition; 
        }
        set{
            _shotPosition = value; 
    }
    }

    public enum PlayerStateEnum
    {
        Idle,                   
        BouncingBall,           
        PreparingToThrow,       
        Throwing,               
        Score,                   
        Miss,                   
        Walking                  
    }

    private PlayerStateEnum _state = PlayerStateEnum.Idle;
    private float _elapsedStateTime = 0.0f;
    private Transform _transform;
    private Animation _animation;
    private CapsuleCollider _collider;
    private bool _holdingBall = false;

    void Awake ()
    {
        _transform = GetComponent<Transform>();
        _animation = GetComponent<Animation>();
        _collider = GetComponent<CapsuleCollider>();
    }

    void Start ()
    {                                             
    }

    void Update ()
    {                        
    }

    public bool IsHoldingBall {
        get {
            return _holdingBall;    
        }
    }

    public PlayerStateEnum State {
        get {
            return _state; 
        }
        set {                        
            _state = value; 
            _elapsedStateTime = 0.0f; 
        }

    }

    public float ElapsedStateTime {
        get {
            return _elapsedStateTime;   
        }
    }       

}


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:

public float MyProperty{
    get {
        return MyPropertyValue; 
    }
    set{
        MyPropertyValue = value;    
    }
}


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.

Variables

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):

public Player player; // Reference to your player on the scene 
public ScoreBoard scoreBoard; // Reference to your games scoreboard 
public Ball basketBall; // 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.Undefined; // 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 count down every second; 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 _orgPlayerPosition;


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 Player object; now add some states for the game:

public enum GameStateEnum
{
    Undefined,
    Menu,
    Paused, 
    Play,
    GameOver
}


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:

public GameStateEnum State {
    get{
        return _state;  
    }
    set{
        _state = value; 

        // MENU 
        if( _state == GameStateEnum.Menu ){
            Debug.Log( "State change - Menu" );               

            player.State = Player.PlayerStateEnum.BouncingBall; 

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


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:

public void StartNewGame(){        
    GamePoints = 0; 
    TimeRemaining = gameSessionTime; 
    player.State = Player.PlayerStateEnum.BouncingBall; 
    State = GameStateEnum.Play;
}


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:

public void ResumeGame(){
    if( _timeRemaining < 0 ){
        StartNewGame();     
    } else{
        State = GameStateEnum.Play;
    }
}


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:

public int GamePoints{
    get{
        return _gamePoints;     
    }
    set{
        _gamePoints = value; 
        scoreBoard.SetPoints( _gamePoints.ToString() ); 
    }
}


This will be responsible for keeping your scoreboard update-to-date with the latest current score.

Finally, add a TimeRemaining property:

public float TimeRemaining {
    get{
        return _timeRemaining; 
    }
    set{            
        _timeRemaining = value; 
        scoreBoard.SetTime( _timeRemaining.ToString("00:00") );           

        // reset the elapsed time 
        _timeUpdateElapsedTime = 0.0f; 
    }
}


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:

void Update () {
    if( _state == GameStateEnum.Undefined ){
        // if no state is set then we will switch to the menu state 
        State = GameStateEnum.Menu; 
    }
    else if( _state == GameStateEnum.Play ){            
        UpdateStatePlay(); 
    }
    else if( _state == GameStateEnum.GameOver ){
        UpdateStateGameOver();  
    }

}

private void UpdateStatePlay(){
    _timeRemaining -= Time.deltaTime; 

    // accumulate elapsed time 
    _timeUpdateElapsedTime += Time.deltaTime; 

    // has a second past? 
    if( _timeUpdateElapsedTime >= 1.0f ){
        TimeRemaining = _timeRemaining; 
    }

    // 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.GameOver;
        } else{             
            // set a new throw position 
            Vector3 playersNextThrowPosition = _orgPlayerPosition;
            // offset x 
            playersNextThrowPosition.x +=  Random.Range(-throwRadius, throwRadius); 
            player.ShotPosition = playersNextThrowPosition;             
        }
    }
}

private void UpdateStateGameOver(){     
    // TODO; to implement (next tutorial)       
}


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.

_timeRemaining -= Time.deltaTime; 

// accumulate elapsed time 
_timeUpdateElapsedTime += Time.deltaTime; 

// has a second past? 
if( _timeUpdateElapsedTime >= 1.0f ){
    TimeRemaining = _timeRemaining; 
}


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.

// 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.GameOver;
    } else{             
        // set a new throw position 
        Vector3 playersNextThrowPosition = _orgPlayerPosition;
        // offset x 
        playersNextThrowPosition.x +=  Random.Range(-throwRadius, throwRadius); 
        player.ShotPosition = playersNextThrowPosition;             
    }
}


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:

public bool IsMobile{
    get{
        return (Application.platform == RuntimePlatform.IPhonePlayer || Application.platform == RuntimePlatform.Android);   
    }
}


Luckily your design requires minimal interaction; all that is required is determining if a finger is down or not, the following snippet does just that.

public int TouchCount {
    get{
        if( IsMobile ){
            return Input.touchCount; 
        } 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 currentTouchDownCount;
        } else{
            if( Input.GetMouseButtonDown(0) ){
                return 1;   
            } else{
                return 0; 
            }
        }
    }
}


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 (http://docs.unity3d.com/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:

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.Miss;

        }
    }
}


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:

void Start () {
    // register the event delegates 
    basketBall.OnNet += HandleBasketBallOnNet;              
}


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:

public void HandleBasketBallOnNet(){
    GamePoints += 3; 
    player.State = Player.PlayerStateEnum.Score;
}


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:

player.OnPlayerAnimationFinished += HandlePlayerOnPlayerAnimationFinished;


Along with its accompanying method:

public void HandlePlayerOnPlayerAnimationFinished (string animationName)
{
    if (player.State == Player.PlayerStateEnum.Walking) {
        player.State = Player.PlayerStateEnum.BouncingBall;
    }
}


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 react to user input; 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.

Note: To find out more about the Animation Component, check out the official Unity documentation here: http://docs.unity3d.com/Documentation/Components/class-Animation.html

Inside the Player script, add some variables to manage the current animation and AnimationClips that will reference the various animations:

private AnimationClip _currentAnimation = null;
public AnimationClip animIdle; 
public AnimationClip animBounceDown; 
public AnimationClip animBounceUp; 
public AnimationClip animWalkForward; 
public AnimationClip animWalkBackward; 
public AnimationClip animPrepareThrow; 
public AnimationClip animThrow; 
public AnimationClip animScore; 
public AnimationClip animMiss;


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:

void Start(){
   _animation = GetComponent<Animation>(); 
   InitAnimations(); 
}

private void InitAnimations ()
{

    _animation.Stop (); 

    _animation [animIdle.name].wrapMode = WrapMode.Once; 
    _animation [animBounceDown.name].wrapMode = WrapMode.Once; 
    _animation [animBounceUp.name].wrapMode = WrapMode.Once;         
    _animation [animWalkForward.name].wrapMode = WrapMode.Loop; 
    _animation [animWalkBackward.name].wrapMode = WrapMode.Loop; 
    _animation [animPrepareThrow.name].wrapMode = WrapMode.Once; 
    _animation [animThrow.name].wrapMode = WrapMode.Once; 
    _animation [animScore.name].wrapMode = WrapMode.Once; 
    _animation [animMiss.name].wrapMode = WrapMode.Once; 

    _animation [animBounceDown.name].speed = 2.0f; 
    _animation [animBounceUp.name].speed = 2.0f;         
}


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:

public bool IsAnimating{
    get{
        return _animation.isPlaying;    
    }
}

public AnimationClip CurrentAnimation {
    get {
        return _currentAnimation; 
    }
    set {
        SetCurrentAnimation (value);   
    }
}

public void SetCurrentAnimation (AnimationClip animationClip)
{
    _currentAnimation = animationClip; 
    _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);    
    }
}


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:

CurrentAnimation = animPrepareThrow;


Then disable the GameController script by unchecking the GameObjects component (as shown below):

And click on the play button; 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.

private void AttachAndHoldBall(){

}


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:

public PlayerStateEnum State{
    get{
        return _state; 
    }
    set{
        CancelInvoke("OnAnimationFinished"); 

        _state = value; 
        _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;
        }                                                                                               
    }
}


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:

CancelInvoke("OnAnimationFinished");

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.Walking; 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:

State = PlayerStateEnum.Score;

As you did before, disable the GameController script by unchecking the GameObjects component so that it does not interfere with your test.

And click on the play button; 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 happen.

Start by declaring the following variables at the top of your Player class:

public Ball basketBall; 
public float bounceForce = 1000f;
private Transform _handTransform;

The variable _handTransform will hold reference to the Transform Component of the bone that will be touching the ball and the bounceForce is used to determine how much force is applied to the ball when bouncing (the basketball variable should be pretty obvious).

One of the first problems to solve is how to position the ball in the player’s hand when the Player’s state changes to BouncingBall. Implement the AttachAndHoldBall method you stubbed out earlier to do that:

public void AttachAndHoldBall ()
{
    _holdingBall = true; 

    Transform bTransform = basketBall.BallTransform; 
    SphereCollider bCollider = basketBall.BallCollider;  
    Rigidbody bRB = basketBall.BallRigidbody; 

    bRB.velocity = Vector3.zero; 

    bTransform.rotation = Quaternion.identity; 

    Vector3 bPos = bTransform.position;         
    bPos = _handTransform.position;
    bPos.y -= bCollider.radius; 
    bTransform.position = bPos;                     

}

One of the publicly exposed variables (named basketball) holds a reference to the basketball object. This function needs a reference to the ball’s transform, collider, and rigid body, so the first part of this method gets those.

With respect to the Rigidbody, any current velocity is removed and the ball (to ensure it has stopped completely and won’t ‘bounce’ out of your hand) is then positioned to the player’s hand using the Ball’s Collider to offset based on the diameter of the ball.

You may be wondering where _handTransform comes from. Recall that you added a Box Collider to one of the Player’s hand when setting up the scene in Part 1.

To make use of this, add the following code to the end of Awake():

_handTransform = _transform.Find (
              "BPlayerSkeleton/Pelvis/Hip/Spine/Shoulder_R/UpperArm_R/LowerArm_R/Hand_R");

This grabs a reference to the appropriate component and attaches to the _transform variable. An alternative would have been to expose it as a public property and assign it via the editor like you’ve done so far, but this is a nice opportunity to demonstrate you how you can traverse a GameObject to obtain reference to one of its children.

Once the Player is holding the ball, he needs to start bouncing it! :]

You do this by holding the ball and playing the BounceUp animation. If, during Update(), the game is in the BouncingBall state, the Player is holding the ball, and the Bounce Down animation has finished playing, then push the ball down via the AddRelativeForce method of the Ball’s Rigidbody using your bounceForce variable. This will force the ball to the ground and make it bounce back up (hence why the force is so high).

Replace the Update method with the following code:

void Update ()
{
    if( _holdingBall ){
        AttachAndHoldBall();    
    }
    _elapsedStateTime += Time.deltaTime; 
}

First you check if the _holdingBall has been set. If it is, you call the AttachAndHoldBall you just implemented above to position the ball in your basketball player’s hand.

The _holdingBall method is set to true within the AttachAndHoldBall method (which in turn is called when the state is changed to BouncingBall) and set to false during bouncing and once thrown the ball.

Next add the following to the end of Update():

if( _state == PlayerStateEnum.BouncingBall ){ 
    if( _holdingBall ){
        if( GameController.SharedInstance.State == GameController.GameStateEnum.Play && GameController.SharedInstance.TouchDownCount >= 1 ){
            State = PlayerStateEnum.PreparingToThrow;
            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 ); 
        }                   
    }
}

The above block (embedded into your Update method) first checks if we’re currently holding the ball, if so asks the GameController if a touch is present. If so, it swaps to the PrepareToThrow state, otherwise checks what animation you’re current playing and if it has finished.

If the down animation has finished then you push the ball to the ground, and if the up animation has finished you start the down animation.

As the ball is bouncing back up, it will collide with the hand’s Box Collider trigger. Implement a method that will be called when this occurs:

public void OnTriggerEnter (Collider collider)
{
    if (_state == PlayerStateEnum.BouncingBall) {
        if (!_holdingBall && collider.transform == basketBall.BallTransform) {
            AttachAndHoldBall ();
            SetCurrentAnimation (animBounceUp);
        }
    }
}

This makes it re-start the bouncing sequence all over again when the ball bounces back up to the hand!

Note that trigger events don’t propagate up to the parent, whereas Collision events do. Therefore this OnTriggerEnter method of the Player Component that you just wrote will not be automatically called when the collision occurs.

However, you can write a helper script written to facilitate this. Create a new script named PlayerBallHandand enter the following code:

using UnityEngine;
using System.Collections;

[RequireComponent (typeof(Collider))]
public class PlayerBallHand : MonoBehaviour
{

    private Player _player = null;

    void Awake ()
    {

    }

    void Start ()
    {
        Transform parent = transform.parent;
        while (parent != null && _player == null) {
            Player parentPlayer = parent.GetComponent<Player>();
            if (parentPlayer != null) {
                _player = parentPlayer;
            } else {
                parent = parent.parent;     
            }
        }
    }

    void Update ()
    {

    }

    void OnTriggerEnter (Collider collider)
    {
        _player.OnTriggerEnter (collider); 
    }
}

This Component is responsible for notifying the Player Component when the ball collides with the hand.

Next switch back to Unity and to attach this script to the Hand_R of the player object. Remember that you created a collider on this object in Part 1 of this tutorial.

Also, select the Player object and attach the basketball to the public variable for it.

And finally, select the BallPhyMat and set the bounciness to 1 so the basketball bounces up with enough force.

Time to test

You’ve written a bit of code so now would be a good time to test that everything is working as expected. As you did before, amend the Start method with the following state to test the bouncing of the ball:

State = PlayerStateEnum.BouncingBall;

Also as you did before, disable the GameController script by unchecking the GameObjects component so that it does not interfere with your test.

And click on the play button; if all is well then you will see the ball bounce up and down as shown below!

Note: Before continuing remember to enable the GameController again and remove the test code snippet.

Throwing the Ball

Start off by declaring the following variables at the top of your Player class:

public float maxThrowForce = 5000f; 
public Vector3 throwDirection = new Vector3( -1.0f, 0.5f, 0.0f );

The maxThrowForce is (as the name suggests) the maximum force you will apply to the ball when throwing it, the amount is relative to how long the user held their finger down (i.e. the longer they held their finger down the larger the proportion of this force will be used). The next variable, throwDirection, determine the angle that you will launch the ball when thrown.

Next add some code to the end of your Update() method to throw the ball at the appropriate time:

if (_state == PlayerStateEnum.PreparingToThrow) {
    if (GameController.SharedInstance.State == GameController.GameStateEnum.Play &&
        GameController.SharedInstance.TouchCount == 0) {

        State = PlayerStateEnum.Throwing;

        _holdingBall = false; 
        basketBall.BallRigidbody.AddRelativeForce (
            throwDirection *
            (maxThrowForce * _animation [animPrepareThrow.name].normalizedTime));
    }
}

Earlier you added some code to your update method to set the Player’s state to “PreparingToThrow” when the ball is bouncing and the player taps the screen.

Now, you’ve added a check to see if you’re in this state and the player releases their finger. You calculate a force to throw the ball based on the time remaining of the associated animation.

normalizedTime is a property of an Animation State which indicates how far the animation has played; 0.0 means the animation is at the start, while 1.0 means the animation has played all the way through.

Next, add the logic to handle the Throwing state:

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

While in the Throwing state you poll to determine when the throw animation has finished and once done so you turn on your collider on to ensure that the ball doesn’t roll through your character.

After the ball is thrown, the Player waits for instructions from the GameController. Based on the result from GameController, a specified animation will play. For instance, if the ball goes through the hoop, a winning animation is played; otherwise, a disappointed animation is played.

Once the play has ended (either missing the hoop and landing on the ground or going through the hoop and landing on the ground), the GameController randomly selects a new shot position and notifies the player to move into position.

Positions Please

Add the following variable to the top of your Player class:

public float walkSpeed = 5.0f;

walkSpeed determines how fast your character will move into his new _shotPosition (set by the GameController).

Also, if you look inside the Player class you’ll see a parameter called shotPosition that you added earlier. This will determine where the Player is standing to perform his shot, and it is updated by the GameController after each shot.

You need to initially set the shot position, so add the following line to the bottom of Awake():

_shotPosition = _transform.position;

Next, override the ShotPosition getter/setter as follows:

public Vector3 ShotPosition{
    get{
        return _shotPosition; 
    }
    set{
        _shotPosition = value; 

        if( Mathf.Abs( _shotPosition.x - _transform.position.x ) < 0.1f ){
            State = PlayerStateEnum.BouncingBall;   
        } else{
            State = PlayerStateEnum.Walking;
        }
    }
}

As mentioned above, the ShotPosition is set by the GameController. This makes it so when the ShotPosition is changed, the Player class checks to see if it represents moving to a new position, and if so changes the state of your Player class to Walking (otherwise, reverts to bouncing ball).

Then for each Update(), you move the player closer, and once close enough, start bouncing the ball again (which means the user can now attempt another shot).

To do this, add the following code to the end of Update():

if (_state == PlayerStateEnum.Walking) {
    Vector3 pos = _transform.position; 
    pos = Vector3.Lerp (pos, _shotPosition, Time.deltaTime * walkSpeed); 
    _transform.position = pos; 

    if ((pos - _shotPosition).sqrMagnitude < 1.0f) {
        pos = _shotPosition;
        if (OnPlayerAnimationFinished != null) {
            OnPlayerAnimationFinished (_currentAnimation.name);
        }
    }
}

Make note of the way that object position and movement is calculated in Unity. If you’ve done game development before, you’re likely aware that in order to keep your game running consistently across all devices, you must update positions and movement relative to the elapsed time. You can access this via the Time static property deltaTime.

deltaTime is the time elapsed since the last update call. Why would this be used to calculate movement on-screen? If you have ever played an old game on a modern computer, you might have noticed that the characters in the game moved around so quickly that you couldn’t control them.

This is because the updates to the character positions were not relative to the elapsed time, but rather a constant. For instance, the distance to move an object 50 pixels is dependent upon many things, including processor speed. However, moving 50 pixels in 0.5 seconds will result in a constant, fluid movement on any platform or processor.

Note: You may be wondering what “lerp” is. lerp is a mathematical function that Linearly intERPolates one value to another. For instance, if you have a start value of 0 and an end value of 10, then by linearly interpolating by 0.5 you would end up with a value of 5. Get comfortable with using lerp; you’ll use it frequently!

And that’s it – you’re finally done, time to test this out! :]

Testing it out!

It’s finally time to test this out! Click the play button to start your game. There are a few things you might have to tweak based on how you have things set up:

You can shoot the ball by clicking and holding in the play area, then releasing. If it goes the wrong way, you can change the Player’s ThrowDirection variable – I had to change mine to X=1, Y=0.75, Z=0. Double check that all the public connections set up in the scripts attached to your Player, Scoreboard, and Game Controller objects are set up correctly.

If you’re still stuck, you can try using the debugger to see what’s wrong! To do this, right click the Inspector tab and choose Debug. Then you can set a breakpoint in MonoDevelop by clicking in the gutter.

Finally, in MonoDevelop go to RunAttach to Process and choose your Unity editor. Then when you play the app, it will pause there when the breakpoint is hit, and you can debug as usual!

If you got it working this far – huge congratulations! At this point, you have a fully functional 3D game to play around with!

Take some time looking over the code – the goal of this tutorial is to provide you with the background of scripting and event handling in Unity.

Where To Go From Here?

Here is a sample project where you have left it off in the tutorial so far. To open it in Unity, go to FileOpen Project, click Open Other, and browse to the folder. Note that the scene won’t load by default – to open it, select ScenesGameScene.

Stay tuned for the third part of this tutorial, you’ll see how to present a simple user interface for the main menu!

In the meantime, if you have any questions or comments on the material presented so far 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/20337/beginning-unity-3d-for-ios-part-2


标签: android

热门推荐