Unity 2020 By Example
上QQ阅读APP看书,第一时间看更新

Creating the player object

We've now imported most assets for the twin-stick shooter, and we're ready to create a player spaceship object. This will be the object that the player will control and move around. Creating this might seem a straightforward matter of simply dragging and dropping the relevant player sprite from the Project panel to the scene, but things are not so simple. The player object is a complex object with many different behaviors, as we'll see shortly. For this reason, more care needs to be taken when creating the player. Let's start with the GameObject, which will contain our custom components.

Creating the GameObject

The GameObject will hold all of the data and components required for our player, including position and collision data, as well as custom functionality we add through writing scripts. To create the player object, perform the following steps:

  1. Create an empty GameObject in the scene by navigating to GameObject | Create Empty from the application menu.
  2. Name the object Player.
  3. The newly created object may or may not be centered at the world origin of (0, 0, 0), and its rotation properties may not be consistently 0 across X, Y, and Z. To ensure a zeroed transform, you could manually set the values to 0 by entering them directly in the Transform component for the object in the Inspector. However, to set them all to 0 automatically, click on the icon with the three dots in the top-left corner of the Transform component and select Reset from the context menu:

    Figure 3.6 – Resetting the Transform component

  4. Drag and drop the Player dropship sprite (in the Textures folder) from the Project panel to the newly created Player object in the Hierarchy panel. Dragging the texture to the player object will make it a child of the player object, which becomes the parent.

    Child/parent hierarchy

    In Unity, you'll often add objects as children of other objects. A child object is positioned, scaled, and rotated relative to their parent. But a parent's transform is not affected by their children, so you can move a child without moving a parent, but if you move a parent object, the child will also move.

Rotate this child object ship by 90 degrees on the X-axis, and 90 degrees on the Z-axis. This rotation orientates the sprite in the direction of its parent's forward vector. Make sure you have the child object selected and not the parent Player object:

Figure 3.7 – Aligning the player ship

You can confirm that the ship sprite has been aligned correctly in relation to its parent by selecting the Player object and viewing the blue forward vector arrow. The front of the ship sprite and the blue forward vector should be pointing in the same direction. If they're not, then continue to rotate the sprite by 90 degrees until they're in alignment. This will be important later when coding player movement to make the ship travel in the direction it's looking:

Figure 3.8 – The blue arrow is called the forward vector

With the GameObject created, visible to the player, and rotated to suit our needs, it's time to add functionality by adding components.

Adding components

We'll prepare the Player object so that it is solid and affected by physical forces. It must collide with other solid objects and take damage from enemy ammo when hit. To facilitate this, two additional components should be added to the Player object, specifically a Rigidbody and Collider:

  1. Select the Player object (not the Sprite object).
  2. Select Component | Physics | Rigidbody from the application menu to add a Rigidbody.
  3. Select Component | Physics | Capsule Collider from the application menu to add a collider.

    2D components

    You may have noticed that there are 2D counterparts to the components we're adding here specifically, Rigidbody2D and Capsule Collider 2D. We'll cover them in Chapter 5, Creating a 2D Adventure Game.

The Collider component approximates the volume of the object, and the Rigibody component uses the collider to determine how physical forces should be applied. Let's adjust the capsule collider a little because the default settings typically do not match up with the Player sprite as intended. Adjust the Direction, Radius, and Height values until the capsule encompasses the Player sprite and represents the volume of the player:

Figure 3.9 – Adjusting the spaceship capsule collider

By default, the Rigidbody component is configured to approximate objects that are affected by gravity, which is not appropriate for a spaceship that flies around. To fix this, Rigidbody should be adjusted as follows:

  1. Disable the Use Gravity checkbox to prevent the object from falling to the ground.
  2. Enable the Freeze Position Y checkbox and the Freeze Rotation Z checkbox to prevent the spaceship moving and rotating around axes that are undesirable in a 2D top-down game, as shown in Figure 3.10:

Figure 3.10 – Configuring the Rigidbody component for the player spaceship

On previewing the game thus far, the spaceship probably looks too large. We can fix this quickly by changing the scale of the Player object. I've used a value of 0.5 for the X, Y, and Z axes, as shown in Figure 3.10.

Excellent work! We've now configured the player spaceship object successfully. Of course, it still doesn't move or do anything specific in the game because we haven't added any code yet. That's something we'll turn to next by adding logic to react to player input.

Controlling the player

The Player object is now created in the scene, configured with both Rigidbody and Collider components. However, this object doesn't respond to player controls. In a twin-stick shooter, the player provides input on two axes and can typically shoot a weapon. The control scheme for our game is outlined here:

  • The keyboard WASD buttons guide player movements up, down, left, and right.
  • The mouse controls the direction in which the player is looking and aiming.
  • The left mouse button fires a weapon.

To implement this, we'll need to create a PlayerController script file. Right-click on the Scripts folder of the Project panel and create a new C# script file named PlayerController.cs:

public class PlayerController : MonoBehaviour

{

   public bool MouseLook = true;

   public string HorzAxis = "Horizontal";

   public string VertAxis = "Vertical";

   public string FireAxis = "Fire1";

   public float MaxSpeed = 5f;

   private Rigidbody ThisBody = null;

   void Awake ()

   {

       ThisBody = GetComponent<Rigidbody>();

   }

}

The following points summarize the code sample:

  • The PlayerController class should be attached to the Player object in the scene. It accepts input from the player and will control the movement of the spaceship.
  • The Awake function is called once when the object is created and is typically used to retrieve references to objects required by the component. You cannot rely on the order in which objects receive an Awake call. During this function, the Rigidbody component (used for controlling player movement) is retrieved. The Transform component can also be used to control player movement through the Position property, but this ignores collisions and solid objects.

Now that we're retrieving the components we require, we can make use of them in the FixedUpdate function:

public class PlayerController : MonoBehaviour

{

    void FixedUpdate ()

    {

        float Horz = Input.GetAxis(HorzAxis);

        float Vert = Input.GetAxis(VertAxis);

        Vector3 MoveDirection = new Vector3(Horz, 0.0f, Vert);

        ThisBody.AddForce(MoveDirection.normalized * MaxSpeed);

        ThisBody.velocity = new Vector3          (Mathf.Clamp(ThisBody.velocity.x, -MaxSpeed,                MaxSpeed),           Mathf.Clamp(ThisBody.velocity.y, -MaxSpeed,             MaxSpeed),          Mathf.Clamp(ThisBody.velocity.z, -MaxSpeed,            MaxSpeed));

        if(MouseLook)

        {

              Vector3 MousePosWorld =                 Camera.main.ScreenToWorldPoint(new                   Vector3(Input.mousePosition.x,                    Input.mousePosition.y, 0.0f));

              MousePosWorld = new Vector3(MousePosWorld.x,                 0.0f, MousePosWorld.z);

              Vector3 LookDirection = MousePosWorld -                 transform.position;

              transform.localRotation = Quaternion.LookRotation

                (LookDirection.normalized,Vector3.up);

        }

    }

}

Let's summarize the preceding code:

  • The FixedUpdate function is called once before the physics system is updated, which is a fixed number of times per second. FixedUpdate differs from Update, which is called once per frame and can vary on a per-second basis as the frame rate fluctuates. For this reason, if you ever need to control an object through the physics system, using components such as Rigidbody, then you should always do so in FixedUpdate and not Update.
  • The Input.GetAxis function is called on each FixedUpdate to read the axial input data from an input device, such as the keyboard or gamepad. This function reads from two named axes, Horizontal (left-right) and Vertical (up-down). These work in a normalized space of -1 to 1. This means that when the left key is pressed and held down, the Horizontal axis returns -1 and, when the right key is being pressed and held down, the horizontal axis returns 1. A value of 0 indicates that either no relevant key is being pressed or both left and right are pressed together, canceling each other out. A similar principle applies to the vertical axis. Up refers to 1, down to -1, and no keypress relates to 0. More information on the GetAxis function can be found online at http://docs.unity3d.com/ScriptReference/Input.GetAxis.html.
  • The Rigidbody.AddForce function applies a physical force to the Player object, moving it in a specific direction. The MoveDirection vector encodes the direction of movement and is based on player input from both the horizontal and vertical axes. This direction is multiplied by our maximum speed to ensure that the force applied to the object is capped. For more information on AddForce, refer to the online Unity documentation at http://docs.unity3d.com/ScriptReference/Rigidbody.AddForce.html.
  • The Camera.ScreenToWorldPoint function converts the screen position of the mouse cursor in the game window to a position in the game world. This code is responsible for making the player always look at the mouse cursor. However, as we'll see soon, some further tweaking is required to make this code work correctly. For more information on ScreenToWorldPoint, refer to the Unity online documentation at http://docs.unity3d.com/ScriptReference/Camera.ScreenToWorldPoint.html.

The preceding code allows you to control the Player object, but there are some problems. One of them is that the player doesn't seem to face the position of the mouse cursor, even though our code is designed to achieve this behavior. The reason is that the camera, by default, is not configured as it needs to be for a top-down 2D game. We'll fix this shortly, but before we move away (no pun intended) from the movement code, let's add one more feature: preventing the player from moving out of the bounds of the game.

Limiting movement

As the game stands now, it's possible to move the player outside the boundaries of the screen. The player can fly off into the distance, out of view, and never be seen again. Not ideal! The player movement should be limited to the camera view or bounds so that it never exits the view.

There are different ways to achieve bounds locking, most of which involve scripting. One way is to clamp the positional values of the Player object between a specified range, a minimum, and a maximum. Consider Code Sample 3.3 for a new C# class called BoundsLock. This script file should be attached to the player:

public class BoundsLock : MonoBehaviour

{

     public Rect levelBounds;

void LateUpdate ()

{

     transform.position = new Vector3            (Mathf.Clamp(transform.position.x, levelBounds.xMin,              levelBounds.xMax), transform.position.y,               Mathf.Clamp(transform.position.z,                levelBounds.yMin, levelBounds.yMax));

}

}

There's not a lot new here that we haven't seen in previous code samples, except possibly the Mathf.Clamp function, which ensures that a specified value is capped between a minimum and maximum range.

Tip

Understanding the order of execution of event functions, such as LateUpdate, is important. I outline the order of execution whenever appropriate in this book, but you can find more information here: https://docs.unity3d.com/Manual/ExecutionOrder.html.

To use the BoundsLock script, perform the following steps:

  1. Drag and drop the file to the Player object.
  2. Specify the bounds in the Inspector:

Figure 3.11 – Setting Bounds Lock

You may be wondering how I came up with those numbers. And it's a good question. I could have used trial and error by setting some initially random numbers, playing the game, refining the numbers, and repeating that process until I had the bounds precisely as I want. However, there's a far more productive way to do it by using Unity's Gizmos.

As discussed in Chapter 1, Exploring the Fundamentals of Unity, we use gizmos all the time in Unity. They add visual representations to GameObjects, imparting additional useful information that will help us develop games. Unity provides many built-in gizmos that make using the editor much easier; for example, the outline of selected objects is a gizmo, if you're using the move tool, that is also a gizmo; even the green outline of a collider is a gizmo. This list goes on, and not only does Unity provide their own gizmos, but we can also write our own.

Important note

Gizmos are only visible in the Unity Editor. They will not be visible to end users, so do not rely on any gizmos for gameplay. For instance, if we wanted the player to see the bounds of the level, a gizmo would not be an appropriate tool for the job. 

We'll use a gizmo to visualize the bounds of the level so that we can see in real time how our settings affect the size and position of the bounds. To do this, add a new function to the BoundsLock script:

public class BoundsLock : MonoBehaviour

{

    void OnDrawGizmosSelected()

    {

    const int cubeDepth = 1;

         Vector3 boundsCenter = new Vector3(levelBounds.xMin +           levelBounds.width * 0.5f, 0, levelBounds.yMin +              levelBounds.height * 0.5f);

Vector3 boundsHeight = new Vector3(levelBounds.             width, cubeDepth, levelBounds.height);

Gizmos.DrawWireCube(boundsCenter, boundsHeight);    

    }

}

In OnDrawGizmosSelected, we call Gizmos.DrawWireCube, which will draw a wireframe of a cube with a specified center and size. The center and size are calculated using the levelBounds rectangle that we created earlier. I've set the cubeDepth arbitrarily to 1 as our game is 2D, and we are not concerned about the depth of the level bounds. As the function name hints, the gizmo will be drawn only if the object is selected in the hierarchy. As we only really need the level bounds visible while we edit them, this is perfect for us.

Tip

OnDrawGizmosSelected is an event function provided by Unity. We could have also used the OnDrawGizmos function. OnDrawGizmos is called every frame and will draw the gizmo even when the object isn't selected, whereas OnDrawGizmosSelected requires the object to be selected. Which function to use depends on your needs. If you want the gizmo to be visible most of the time, then OnDrawGizmos is more appropriate. If, however, you only need it to be shown when a specific object is selected, such as in our level bounds example, then OnDrawGizmosSelected is more appropriate.

To test whether the gizmo is working correctly, in the Unity Editor, select the Player object. As the BoundsLock script is attached to that object, a white cube wireframe should be drawn in the Scene view, as shown in Figure 3.12:

Figure 3.12 – Level bounds displayed using a gizmo

If you edit the Level Bounds rectangle on the Player object, you'll notice that the gizmo's size is automatically adjusted to reflect the new level bounds. Perfect! Using this, you can easily customize the level bounds to suit your needs.

Tip

Gizmos can also be viewed in the Game tab by selecting the gizmo button on the panels toolbar. However, as previously mentioned, even with this setting turned on, they will not be visible in the final compiled game.

Now take the game for a test run by pressing Play on the toolbar. The player spaceship should remain in view and be unable to move offscreen. Splendid!

As stated earlier, you may have an issue where the player object does not face the cursor correctly. Because the project was initially created as a 3D project, the camera is not configured correctly for our needs, so we will need to change that now and, in the process, learn how easy it is to switch a camera from a 3D to a 2D perspective.