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

Creating the enemy object

The enemies in our game will take the form of roaming spaceships that will be spawned into the scene at regular intervals and will follow the player, drawing nearer and nearer. Essentially, each enemy represents a combination of multiple behaviors working together, and these should be implemented as separate scripts. Let's consider them in turn:

  • Health: Each enemy supports health functionality. They begin the scene with a specified amount of health and will be destroyed when that health falls below 0. We already have a health script created to handle this behavior.
  • Movement: Each enemy will continuously be in motion, traveling in a straight line along a forward trajectory.
  • Turning: Each enemy will rotate and turn toward the player even as the player moves. In combination with the movement functionality, this will ensure that the enemy is always moving toward the player.
  • Scoring: Each enemy rewards the player with a score value when destroyed.
  • Damage: Enemies cannot shoot but will harm the player on collision.

Now that we've identified the range of behaviors applicable to an enemy, let's create an enemy in the scene.

Creating the GameObject

We'll make one specific enemy, create a prefab from that, and use it as a basis to instantiate multiple enemies:

  1. Start by selecting the player character in the scene and duplicating the object with Ctrl + D or select Edit | Duplicate from the application menu. We will edit this duplicate to become our enemy.
  2. Rename the object to Enemy and ensure that it is not tagged as Player, as there should be one and only one object in the scene with the Player tag. Set the tag to Untagged, as shown in Figure 3.21:

    Figure 3.21 – Removing a Player tag from the enemy, if applicable

  3. Temporarily disable the Player GameObject, allowing us to focus more clearly on the Enemy object in the Scene tab.
  4. Select the child object of the duplicated enemy and, from the Inspector, click on the Sprite field of the Sprite Renderer component to pick a new sprite. Pick one of the darker imperial ships for the enemy character, and the sprite will update for the object in the viewport:

    Figure 3.22 – Selecting a sprite for the Sprite Renderer component

  5. After changing the sprite to an enemy character, you may need to adjust the Rotation values to align the sprite to the parent forward vector, ensuring that the sprite is looking in the same direction as the forward vector, as shown in Figure 3.22.
  6. Lastly, select the parent object for the enemy and remove the Rigidbody, PlayerController, and BoundsLock components, but keep the Health component as the enemy should support health. See Figure 3.23 for the updated component list. Also, feel free to resize the Capsule Collider component to better approximate the Enemy object:

Figure 3.23 – Adjusting enemy sprite rotation

Tip

When designing scripts for your games, try to create them in such a way that they can be reused as much as possible, such as our Health component. If we had added logic specific to the player in the Health component, we wouldn't have been able to add it to both our enemy and player objects. We would most likely have ended up writing duplicate code for the enemy objects.

Now that we have the base enemy object, we can start adapting its behavior with custom scripts, starting with moving the enemy to chase the player.

Moving the enemy

As a reminder, the enemy should continually move in the forward direction at a specified speed. To achieve this, create a new script called Mover.cs and add it to the Enemy object:

public class Mover : MonoBehaviour

{

     public float MaxSpeed = 10f;

     void Update ()

     {

         transform.position += transform.forward * MaxSpeed *           Time.deltaTime;

     }

}

The following points summarize the code sample:

  • The script moves an object at a specified speed (MaxSpeed per second) along its forward vector. To do this, it uses the Transform component.
  • The Update function is responsible for updating the position of the object. It multiplies the forward vector by the object speed and adds this to its existing position to move the object further along its line of sight. The Time.deltaTime value is used to make the motion frame rate independent style moving the object per second as opposed to per frame. More information on deltaTime can be found at http://docs.unity3d.com/ScriptReference/Time-deltaTime.html.

It's always good practice to frequently test your code. Your enemy may move too slow or too fast. So, we'll do that now by pressing Play on the toolbar. If the enemy's speed isn't correct, perform the following steps:

  1. Stop playback to exit game mode.
  2. Select the enemy in the scene.
  3. From the Inspector, adjust the Max Speed value of the Mover component:

Figure 3.24 – Adjusting enemy speed

The enemy will now move forward, but this won't be much of a challenge for the player, as it will be effortless to avoid. To increase the difficulty, let's make the enemy turn toward the player. Turning toward the player, combined with moving in its forward direction, will create a suitable chase mechanic.

Turning the enemy

In addition to moving in a straight line, the enemy should also continually turn to face the player. To achieve this, we'll write another script that works in a similar manner to the player controller script, but instead of turning to face the cursor, the enemy turns to face the player. This functionality should be encoded in a new script file called ObjFace.cs and, once again, be attached to the enemy object:

public class ObjFace : MonoBehaviour

{

    public Transform ObjToFollow = null;

    public bool FollowPlayer = false;

    void Awake ()

    {

        if(!FollowPlayer)         {            return;         }

        GameObject PlayerObj =

          GameObject.FindGameObjectWithTag("Player");

        if(PlayerObj != null)         {            ObjToFollow = PlayerObj.transform;

        }     }

     void Update ()

     {

         if(ObjToFollow==null)

         {

             return;

         }

         //Get direction to follow object

         Vector3 DirToObject = ObjToFollow.position -            transform.position;

         if(DirToObject != Vector3.zero)

         {

             transform.localRotation = Quaternion.LookRotation

              (DirToObject.normalized,Vector3.up);

         }

      }

}

The following points summarize the code sample:

  • The ObjFace script will always rotate an object so that its forward vector points toward a destination point in the scene.
  • In the Awake event, the FindGameObjectWithTag function retrieves a reference to the only object in the scene tagged as Player, which should be the player spaceship. The player represents the default look-at destination for an enemy object.
  • The Update function is called automatically once per frame and will generate a displacement vector from the object location to the destination location. This vector represents the direction in which the object should be looking. The Quaternion.LookRotation function accepts a direction vector and will rotate an object to align the forward vector with the supplied direction. This keeps the object looking toward the destination. More information on LookRotation can be found at http://docs.unity3d.com/ScriptReference/Quaternion.LookRotation.html.

Before testing the code, make sure of the following:

  • The Player object in the scene is tagged as Player.
  • The Player object is enabled (we previously disabled the object for testing).
  • The enemy's position is offset from the player.
  • The Follow Player checkbox on the Obj Face component is enabled in the Inspector.

Figure 3.25 shows these settings in practice:

Figure 3.25 – Enemy spaceship moving toward the player

This is looking excellent! However, it's not very challenging if the enemy doesn't damage the player when they collide, so we will resolve that next.

Dealing damage to the player

If and when the enemy finally collides with the player, it should deal damage and potentially kill the player. To achieve this, a collision between the enemy and player must be detected. Let's start by configuring the enemy. Select the Enemy object and, from the Inspector, enable the Is Trigger checkbox on the Capsule Collider component, as shown in Figure 3.26:

Figure 3.26 – Changing the enemy collider to a trigger

Setting a collider as a trigger means that we can still respond to collision events, but Unity will not try to resolve (separate) the collisions.

Next, we'll create a script that detects collisions and deals damage to the player for as long as the collision state remains. Refer to the following code (ProxyDamage.cs), which should be attached to the enemy character:

public class ProxyDamage : MonoBehaviour

{

     //Damage per second

     public float DamageRate = 10f;

     void OnTriggerStay(Collider Col)

     {

         Health H = Col.gameObject.GetComponent<Health>();

         if(H == null)

         {

              return;

         }

         H.HealthPoints -= DamageRate * Time.deltaTime;

     }

}

The following points summarize the code sample:

  • The ProxyDamage script will deal damage to any colliding object with a Health component.
  • The OnTriggerStay event is called once every frame for as long as an intersection state persists. During this function, the HealthPoints value of the Health component is reduced by the DamageRate, which is multiplied by Time.deltaTime to get the damage per second (DPS).

After attaching the ProxyDamage script to an enemy, you can use the Inspector to set the Damage Rate of the Proxy Damage component. The Damage Rate represents how much health should be reduced on the player, per second, during a collision. For a challenge, I've set the value to 100 health points:

Figure 3.27 – Setting Damage Rate for a Proxy Damage component

Time for a test run: press Play on the toolbar and attempt a collision between the player and enemy. After 1 second, the player should be destroyed. Things are coming along well. However, we'll need more than one enemy to make things challenging.