Book Image

Unity 4 Game Development HOTSHOT

By : Jate Wittayabundit
Book Image

Unity 4 Game Development HOTSHOT

By: Jate Wittayabundit

Overview of this book

<p>Immerse yourself in the world of high-end game design by partaking in challenging missions. Start off by working with the Sprite Mode, then learn the basics of creating a UI system for an RPG, and work your way through the game virtually embodying your greatest hero or heroine.</p> <p>Every project is designed to push your Unity skills to the limit and beyond. You will start by creating a 2D platform game with the new 2D sprite feature and move on to the Unity GUI system. Then, you will create a 3D character and make it move. By the end of this book, you will know how to post the player's score to the hi-score board.</p>
Table of Contents (19 chapters)
Unity 4 Game Development HOTSHOT
Credits
About the Author
About the Reviewers
www.PacktPub.com
Preface
Index

Controlling the character with the PlayerController_2D class


In this section, we will create a new script to control the movement of our character and a sprite animation for each action of our character. We will use MonoDevelop as our scripting editor, which comes with Unity. MonoDevelop is mainly designed for the C# and .NET environments, so if you are comfortable with C#, you will probably love it. However, if you use Unity JavaScript, it also has many tools to help us write the script faster and debug better, such as finding as well as replacing words in the whole project by pressing command + Shift + F in Mac or Ctrl + Shift + F in Windows and autocomplete (or Intellisense), to name a few. Moving from Unity JavaScript to C# or C# to Unity JavaScript is also a comparatively smooth transition.

Note

In this version of this book, we will show examples for both Unity JavaScript and C#. You can check out the major difference between Unity JavaScript and C# in the Appendix C, Major differences Between C# and Unity JavaScript.

Prepare for lift off

Now, we are just about to start coding, but first let's make sure that we have everything ready:

  1. Next, we want to make sure that Unity uses MonoDevelop as our main Scripting editor (Unity | Preferences in Mac or Edit | Preferences in Windows).

  2. We will see a Unity Preferences window. In the External Tools tab, go to the External Script Editor and make sure that the MonoDevelop option is selected. Click on Browse…, go to Applications | Unity | MonoDevelop.app in Mac or {unity install path} | Unity | MonoDevelop | MonoDevelop.exe in Windows, and we are done:

If we develop a game for Android, we can also set the Android SDK Location path here as shown in the previous screenshot.

Engage thrusters

Now, we are ready to create the PlayerController_2D script. Let's get started:

  1. First, go to Assets | Create | Javascript (for Unity JavaScript developers) or Assets | Create | C# Script (for C# developers) and name our script as PlayerController_2D.

  2. Double-click on the script; it will open the MonoDevelop window.

  3. Now, we will see three windows in the MonoDevelop screen:

    • On the top-left is Solution; we can see our project folder here, but it will only show the folder that contains a script

    • On the bottom-left, we will see a Document Outline; this window will show all the functions, classes, and parameters in the file

    • The last window on the right will be used to type our code

  4. Let's get our hands dirty with some code—first start with defining the following functions to initialize the variables: the Awake() and Start() function.

    Tip

    Awake ()

    Awake () is called when the script instance is being loaded. It used to initialize any variable or game state before calling the Start() function. In the Awake() function, we usually put any GetComponent() or Find() object function, which will make it easier to set up all the parameters during Start().

  5. We need to remove the autogenerated code and replace it with the following code:

    // Unity JavaScript user:
    
    #pragma strict
    @script RequireComponent(AudioSource)
    @script RequireComponent(BoxCollider2D)
    @script RequireComponent(Rigidbody2D)
    // Distance from the character position to the ground
    private final var RAYCAST_DISTANCE : float = 0.58f;
    // This number should be between 0.35 to 0.49
    private final var CHARACTER_EDGE_OFFSET : float = 0.40f;	
    var doorOpenSound : AudioClip;
    var getKeySound : AudioClip;
    var jumpSound : AudioClip;
    var moveForce : float = 80f;
    var jumpForce : float = 350f;
    var maxSpeed : float = 3f;
    var layerMask : LayerMask;
    private var _animator : Animator;
    private var _boxCollider2D : BoxCollider2D;
    private var _isFacingRight : boolean = true;
    private var _isJump : boolean = false;
    private var _isFall : boolean = false;
    private var _isGrounded : boolean = false;
    private var _gameEnd : boolean = false;
    private var _height : float;
    private var _lastY : float;
    private var _horizontalInput : float;
    function Awake() {
      _animator = GetComponent.<Animator>();
      _boxCollider2D = GetComponent.<BoxCollider2D>();
      _height = _boxCollider2D.size.y;
    }
    function Start () {
      _isFacingRight = true;
      _isJump = false;
      _isFall = false;
      _isGrounded = false;
      _gameEnd = false;
      _lastY = transform.position.y;
      Camera.main.transform.position = new Vector3(transform.position.x, transform.position.y, Camera.main.transform.position.z);
    }

    Note

    #pragma strict

    In Unity JavaScript, we can use #pragma strict to tell Unity to disable dynamic typing (var name = 5) and force us to use static typing (var name : int = 5). This will make it easy for us to debug. For example, if we forgot to use static typing, you will see an error message from the Unity console window. Using strict typing also makes the code run faster as the complier doesn't have to go and do the type lookups.

    // C# user:
    
    using UnityEngine;
    using System.Collections;
    [RequireComponent(typeof(AudioSource))]
    [RequireComponent(typeof(BoxCollider2D))]
    [RequireComponent(typeof(Rigidbody2D))]
    public class PlayerController_2D : MonoBehaviour
    {
      // Distance from the character position to the ground
      const float RAYCAST_DISTANCE = 0.58f;
      // This number should be between 0.35 to 0.49
      const float CHARACTER_EDGE_OFFSET = 0.40f;
      
      public AudioClip doorOpenSound;
      public AudioClip getKeySound;
      public AudioClip jumpSound;
      public float moveForce = 80f;
      public float jumpForce = 350f;
      public float maxSpeed = 3f;
      public LayerMask layerMask; 
      Animator _animator;
      BoxCollider2D _boxCollider2D;
      bool _isFacingRight = true;
      bool _isJump = false;
      bool _isFall = false;
      bool _isGrounded = false;
      bool _gameEnd = false;
      float _height;
      float _lastY;
      float _horizontalInput;
      void Awake() {
        _animator = GetComponent<Animator>();
        _boxCollider2D = GetComponent<BoxCollider2D>();
        _height = _boxCollider2D.size.y;
      }
      void Start () {
        _isFacingRight = true;
        _isJump = false;
        _isFall = false;
        _isGrounded = false;
        _gameEnd = false;
        _lastY = transform.position.y;
        Camera.main.transform.position = new Vector3(transform.position.x, transform.position.y, Camera.main.transform.position.z);
      } 
    }

    Tip

    Downloading the example code

    You can download the example code files for all Packt books you have purchased from your account at http://www.packtpub.com. If you purchased this book elsewhere, you can visit http://www.packtpub.com/support and register to have the files e-mailed directly to you.

    Note

    @script RequireComponent(Component) and [RequireComponent(typeof(Component))]

    We add RequireComponent to force the script to automatically add the required component as a dependency when adding this class to the game object. For more details, visit http://docs.unity3d.com/Documentation/ScriptReference/RequireComponent.html.

  6. Next, we will add the Flip() function after the Start() function to make our character sprite show the correct graphics when moving left or right. The code for Unity JavaScript and C# users are as follows:

    // Unity JavaScript user:
    
    private function Flip () {
      _isFacingRight = !_isFacingRight;
      var scale : Vector3 = transform.localScale;
      scale.x *= -1;
      transform.localScale = scale;
    }
    
    
    // C# user: (Put the code inside the class)
    
    void Flip () {
      _isFacingRight = !_isFacingRight;
      Vector3 scale = transform.localScale;
      scale.x *= -1;
      transform.localScale = scale;
    }
  7. Next, we will add another function, which will use Physics2D.Raycast to check whether the player is on the ground or not. Let's create the Grounded() function as follows:

    // Unity JavaScript user:
    
    private function Grounded () {
      var distance : float = _height*RAYCAST_DISTANCE;
      var hitDirectionV3 : Vector3 = transform.TransformDirection(-Vector3.up);
      var hitDirection : Vector2 = new Vector2(hitDirectionV3.x,hitDirectionV3.y);	
      var rightOrigin : Vector2 = new Vector2(transform.position.x + (_boxCollider2D.size.x*CHARACTER_EDGE_OFFSET), transform.position.y);
      var leftOrigin : Vector2 = new Vector2(transform.position.x - (_boxCollider2D.size.x*CHARACTER_EDGE_OFFSET), transform.position.y);
      var origin : Vector2 = new Vector2(transform.position.x, transform.position.y);
      if (Physics2D.Raycast (origin, hitDirection, distance, layerMask.value)) {
        _isGrounded = true;
      } else if (Physics2D.Raycast (rightOrigin, hitDirection, distance, layerMask.value)) {
        _isGrounded = true;
      } else if (Physics2D.Raycast (leftOrigin, hitDirection, distance, layerMask.value)) {
        _isGrounded = true;
      } else {
        if (_isGrounded) {
          if (Mathf.Floor(transform.position.y) == _lastY) {
            _isGrounded = true;
          } else {
            _isGrounded = false;
          }
        }
      }
      _lastY = Mathf.Floor(transform.position.y);
    }
    
    
    // C# user: (Put the code inside the class)
    
    void Grounded () {
      float distance = _height*RAYCAST_DISTANCE;
      Vector3 hitDirectionV3 = transform.TransformDirection (-Vector3.up);
      Vector2 hitDirection = new Vector2(hitDirectionV3.x,hitDirectionV3.y);
      Vector2 rightOrigin = new Vector2(transform.position.x + (_boxCollider2D.size.x*CHARACTER_EDGE_OFFSET), transform.position.y);
      Vector2 leftOrigin = new Vector2(transform.position.x - (_boxCollider2D.size.x*CHARACTER_EDGE_OFFSET), transform.position.y);
      Vector2 origin = new Vector2(transform.position.x, transform.position.y);
      if (Physics2D.Raycast (origin, hitDirection, distance, layerMask.value)) {
        _isGrounded = true;
      } else if (Physics2D.Raycast (rightOrigin, hitDirection, distance, layerMask.value)) {
        _isGrounded = true;
      } else if (Physics2D.Raycast (leftOrigin, hitDirection, distance, layerMask.value)) {
        _isGrounded = true;
      } else {
        if (_isGrounded) {
          if (Mathf.Floor(transform.position.y) == _lastY) {
            _isGrounded = true;
          } else {
            _isGrounded = false;
          }
        }
      } 
      _lastY = Mathf.Floor(transform.position.y);
    }
  8. Next, we will include the script in the Update() function and LateUpdate() function that get called after the Update() function. These functions will be used to check the input from the user and update the camera to follow our character movement. The code for these functions are as follows:

    // Unity JavaScript user:
    
    function Update() {
      if (!_gameEnd) {
        _horizontalInput = Input.GetAxis("Horizontal");
        if ((!_isFacingRight && (_horizontalInput > 0)) || 
          (_isFacingRight && (_horizontalInput < 0))) {
          Flip();
        }
        Grounded();
        if(Input.GetButtonDown("Jump") && _isGrounded) {
          _isJump = true;
        }
      }
    }
    function LateUpdate() {
      if (!_gameEnd) {
        Camera.main.transform.position = new Vector3(transform.position.x, transform.position.y, Camera.main.transform.position.z);
      }
    }
    
    
    // C# user: (Put the code inside the class)
    
    void Update() {
      if (!_gameEnd) {
        _horizontalInput = Input.GetAxis("Horizontal");
        if ((!_isFacingRight && (_horizontalInput > 0)) || 
          (_isFacingRight && (_horizontalInput < 0))) {
          Flip();
        }
        Grounded();
        if(Input.GetButtonDown("Jump") && _isGrounded) {
          _isJump = true;
        }
      }
    }
    void LateUpdate() {
      if (!_gameEnd) {
        Camera.main.transform.position = new Vector3(transform.position.x, transform.position.y, Camera.main.transform.position.z);
      }
    }
  9. Then, we create a FixedUpdate() function, which will handle the animation state changing (Idle, Walk, Jump, or Fall) and add force to move our character horizontally and vertically. Let's add this function as follows:

    // Unity JavaScript user:
    
    function FixedUpdate () {
      if (!_gameEnd) {
        _animator.SetFloat("Speed", Mathf.Abs(_horizontalInput));
        var xSpeed : float = Mathf.Abs(_horizontalInput * rigidbody2D.velocity.x);
        if (xSpeed < maxSpeed) {
          rigidbody2D.AddForce(Vector2.right * _horizontalInput * moveForce);
        }
        if (Mathf.Abs(rigidbody2D.velocity.x) > maxSpeed) {
          var newVelocity : Vector2 = rigidbody2D.velocity;
          newVelocity.x = Mathf.Sign(newVelocity.x) * maxSpeed;
          rigidbody2D.velocity = newVelocity;
        }
        if(_isJump) {
          _animator.SetTrigger("Jump");
          audio.volume = 0.3f;
          audio.PlayOneShot(jumpSound);	
          rigidbody2D.AddForce(new Vector2(0f, jumpForce));   
          _isJump = false;
        }
        if (!_isGrounded) {
          if ((rigidbody2D.velocity.y <= 0f) && !_isFall) {
            _animator.SetTrigger("Fall");
            _isFall = true;
          }
        }
        if (_isGrounded) {
          if (_isFall) {
            _animator.SetTrigger("Ground");
            _isFall = false;
          } else {
            var animationStateInfo : AnimatorStateInfo = _animator.GetCurrentAnimatorStateInfo(0);
            if ((rigidbody2D.velocity.y < 0f) && (animationStateInfo.IsName("Base Layer.Jump"))) {
              _animator.SetTrigger("Fall");
              _isFall = true;
            }
          }
        }
      }
    }
    
    
    // C# user: (Put the code inside the class)
    
    void FixedUpdate () {
      if (!_gameEnd) {
        #region Setting player horizontal movement
        _animator.SetFloat("Speed", Mathf.Abs(_horizontalInput));
        float xSpeed = Mathf.Abs(_horizontalInput * rigidbody2D.velocity.x);
        if (xSpeed < maxSpeed) {
          rigidbody2D.AddForce(Vector2.right * _horizontalInput * moveForce);
        }
        if (Mathf.Abs(rigidbody2D.velocity.x) > maxSpeed) {
          Vector2 newVelocity = rigidbody2D.velocity;
          newVelocity.x = Mathf.Sign(newVelocity.x) * maxSpeed;
          rigidbody2D.velocity = newVelocity;
        }
        #endregion
        #region If the player should jump
        if(_isJump) {
          _animator.SetTrigger("Jump");
          audio.volume = 0.3f;
          audio.PlayOneShot(jumpSound);	
          rigidbody2D.AddForce(new Vector2(0f, jumpForce));	
          _isJump = false;
        }
        #endregion
        #region If the player should fall
        if (!_isGrounded) {
          if ((rigidbody2D.velocity.y <= 0f) && !_isFall) {
            _animator.SetTrigger("Fall");
            _isFall = true;
          }
        }
        #endregion
        #region If the player is grounded
        if (_isGrounded) {
          if (_isFall) {
            _animator.SetTrigger("Ground");
            _isFall = false;
          } else {
            AnimatorStateInfo animationStateInfo = _animator.GetCurrentAnimatorStateInfo(0);
            if ((rigidbody2D.velocity.y < 0f) && (animationStateInfo.IsName("Base Layer.Jump"))) {
              _animator.SetTrigger("Fall");
              _isFall = true;
            }
          }
        }
        #endregion
      }
    }

    Tip

    #region YourComment - #endregion

    In C#, we can put any script between #region Your Code Discription and #endregion to specify a block of code that we can expand or collapse by clicking on the plus and minus sign in MonoDevelop.

    We can see how to use #region ….. #endregion in the following screenshot:

    Lastly, we will add the OnDrawGizmos() function in this class. It is a very nice function that allows us to debug the game, the result of which we won't see in the real game. Let's add the following block of code:

    // Unity JavaScript user:
    
    function OnDrawGizmos() {
      _boxCollider2D = GetComponent.<BoxCollider2D>();
      _height = _boxCollider2D.size.y;
      var distance : float = (_height * RAYCAST_DISTANCE);
      var rightOrigin : Vector3 = new Vector3(transform.position.x + (_boxCollider2D.size.x*CHARACTER_EDGE_OFFSET), transform.position.y, transform.position.z);
      var leftOrigin : Vector3 = new Vector3(transform.position.x - (_boxCollider2D.size.x*CHARACTER_EDGE_OFFSET), transform.position.y, transform.position.z);
      Gizmos.color = Color.red;
      Gizmos.DrawRay(transform.position, transform.TransformDirection (-Vector3.up) * distance);
      Gizmos.DrawRay(rightOrigin, transform.TransformDirection (-Vector3.up) * distance);
      Gizmos.DrawRay(leftOrigin, transform.TransformDirection (-Vector3.up) * distance);
    }
    
    
    // C# user: (Put the code inside the class)
    void OnDrawGizmos() {
      _boxCollider2D = GetComponent<BoxCollider2D>();
      _height = _boxCollider2D.size.y;
      float distance = (_height * RAYCAST_DISTANCE);
      Vector3 rightOrigin = new Vector3(transform.position.x + (_boxCollider2D.size.x*CHARACTER_EDGE_OFFSET), transform.position.y, transform.position.z);
      Vector3 leftOrigin = new Vector3(transform.position.x - (_boxCollider2D.size.x*CHARACTER_EDGE_OFFSET), transform.position.y, transform.position.z);
      Gizmos.color = Color.red;
      Gizmos.DrawRay(transform.position, transform.TransformDirection (-Vector3.up) * distance);
      Gizmos.DrawRay(rightOrigin, transform.TransformDirection (-Vector3.up) * distance);
      Gizmos.DrawRay(leftOrigin, transform.TransformDirection (-Vector3.up) * distance);
    }
  10. Now, save it and go back to Unity; drag-and-drop the PlayerController_2D script to Player, click on Player, and go to the Inspector window. Click on Idle Sprite and Walk Sprite to expand it and then set the following:

    Player Controller_2D (Script)

    Door Open Sound

    doorOpen

    Get Key Sound

    getkey

    Jump Sound

    jump

    Layer Mask

    Ground

Yes! We are done. Let's click on the Play button to play the game. We will see our player moving his hand back and forth. Next, press the A or left arrow / D or right arrow to move the player to the left or to the right; now we see that he is walking. We can also press the Space bar to make our character jump. Isn't that cool?

Objective complete – mini debriefing

We just created a script that controls the movement of our character and his animation. First, we created the parameters to use in our script. We also used the const keyword in C# and the final keyword in Unity JavaScript to create the constant variables.

Note

The const and final keywords

We used the const keyword in C# to specify that the value of the field or local variable is constant, which means it can't be modified from anywhere else.

However, there is no const keyword in Unity JavaScript, so we use the final keyword instead; there is a slight difference between the final and const keywords. The const keyword can only applied to a field whose value is to be known at compile time, such as const float. So, this means that we can't use the const keyword with Vector3, Rect, Point, and all the struct that are included in Unity. On the other hand, we will use readonly instead. However, the final keyword can be used in the both cases.

For more details, visit http://tutorials.csharp-online.net/CSharp_FAQ%3A_What_are_the_differences_between_CSharp_and_Java_constant_declarations.

Then, we get _animator, _boxCollider2D, and _height in the Awake() function to check the animation state while it is moving. Then, we set all the variables to their default value. Next, we created the Flip() function, which will set _isFacingRight to true or false depending on the character movement. This function also sets the local x scale to 1 if the character is facing right and -1 if it's facing left.

The next function that we created is the Grounded() function that uses Physics2D.Raycast to check whether our character is on the ground or not.

In the Update() function, we get the character's horizontal movement by using Input.GetAxis("Horizontal"). Then, we check for the movement direction and call the Flip() function. After that, we call the Grounded() function to check whether the character is on the ground or not. If not, we can make the character jump by using Input.GetButtonDown("Jump"). Next, we update our camera position by using Camera.main to get access to the Main Camera object and set its position relative to Player.

Tip

Camera.main lets us access a Camera object that has the MainCamera tag from anywhere we want.

In the FixedUpdate() function, we change the animation state from Idle to Walk by using _animator.SetFloat("Speed", Mathf.Abs(_horizontalInput));. Next, we get the x speed and check that the speed is lower than the maximum speed. Then, we add the force to Rigidbody2D by using rigidbody2D.AddForce(Vector2.right * _horizontalInput * moveForce);. We also make sure that the velocity doesn't reach the maximum limit after adding the force by assigning newVelocity to rigidbody2D.velocity.

Next, we trigger the jump animation state by using _animator.SetTrigger("Jump");. We also add the force to make our character jump using rigidbody2D.AddForce(new Vector2(Of,jumpForce);. Then, we trigger the fall animation state by using _animator.SetTrigger("Fall");. We also trigger the ground animation state by using _animator.SetTrigger("Ground"); if our character is falling.

Then, we check for the current animation state by getting animatorStateInfo using _animator.GetCurrentAnimatorStateInfo(0);. Then, if the current state is the jump state and y velocity lower than 0, we will change the animation state to the fall state using if ((rigidbody2D.velocity.y < 0) && (animationStateInfo.IsName("BaseLayer.Jump")).

At last, we use the OnDrawGizmos() function to see the visual of the ray that was drawn in the Grounded() function using Physics2D.Raycast. We can also use this function to debug the game without removing any code when releasing the game. Because all the code in the OnDrawGizmos() function won't be shown in the real game, it's a convenient way to debug our game. As we can see from the following figure, the red arrows represent where the raycast is, which will only show in the scene view:

Classified intel

In Unity, we can set a custom Input Manager by going to Edit | Project Settings | Input. In Inspector, click on Axes and you will see that the value of Size is 15, which is the array length of all the inputs. If we want more than 15 inputs, we can put the number here (the default is 15). Next, we will see all 15 names from Horizontal to Jump as a default setting. Each one will have its own parameters, which we can set up as follows:

For more information of each parameter, go to the following link: http://docs.unity3d.com/Manual/class-InputManager.html.

The Negative Button and Positive Button here will send the negative and positive value, which in most cases is used to control directions such as, left, right, up, and down. There is a Dead parameter, which will set any number that is lower than this parameter to 0, which is very useful when we use a joystick.

Also, setting Type to Key or Mouse Button and enabling the Snap parameter will reset axis values to zero after it receives opposite inputs.

Physics2D.Raycast

If we take a look at the Grounded() function and check out the following highlighted code, we will see that we have cast the ray a bit longer than the sprite collider:

if (Physics2D.Raycast (origin, hitDirection, distance, layerMask.value)) {
  _isGrounded = true;
} else if (Physics2D.Raycast (rightOrigin, hitDirection, distance, layerMask.value)) {
  _isGrounded = true;
} else if (Physics2D.Raycast (leftOrigin, hitDirection, distance, layerMask.value)) {
  _isGrounded = true;
}

From the highlighted code, we draw Raycast from the middle of our character downward by 0.58 units. Why don't we set it to 0.5? We set it this way to make our character stay on the edge of the floor surface. If we set it to 0.5, we will see that the character will not hit the ground. This is because of the way Box Collider 2D and Polygon Collider 2D detect other collider objects in Unity, as we can see in the following screenshot:

One last thing for Gizmos: if we want to see our gizmos in the game scene, we can click on the Gizmos tab on the top-right corner of the game scene: