Gigi Labs

Please follow Gigi Labs for the latest articles.
Showing posts with label prefabs. Show all posts
Showing posts with label prefabs. Show all posts

Saturday, June 1, 2013

Unity3D: Space Invaders (Part 5 - Behaviour)

Hi all! :)

In yesterday's article - Unity3D: Space Invaders (Part 4 - Collisions) - we learned how to handle collisions in Unity. At the end of the article, we listed several items that are still missing from Ranch Invaders.

Today's article deals with some of those issues, mainly relating to Player and Alien behaviour.

Player Movement

In Space Invaders, the Player's ship moves only left and right. If you've read "Unity3D: Moving an Object with Keyboard Input", you'll know that this is really easy to do. Open the Player script with MonoDevelop. You should currently have the code for shooting:

    void Update ()
    {
        if (Input.GetKeyDown(KeyCode.Space))
        {
            Instantiate(bullet, this.transform.position, Quaternion.identity);
        }
    }

Change it as follows to allow the Player to move using the left and right arrows:

    void Update ()
    {
        Vector3 pos = this.transform.position;
       
        if (Input.GetKeyDown(KeyCode.Space))
            Instantiate(bullet, this.transform.position, Quaternion.identity);
        if (Input.GetKeyDown(KeyCode.LeftArrow))
            this.transform.position = new Vector3(pos.x - 0.1f, pos.y, pos.z);
        if (Input.GetKeyDown(KeyCode.RightArrow))
            this.transform.position = new Vector3(pos.x + 0.1f, pos.y, pos.z);
    }

Great, you can now move the Player's ship left and right. There's a problem, however. If you hold down an arrow key, the Player's ship does not continue moving. We can fix this by using Input.GetKey() instead. Also, we need to set constraints to prevent the Player's ship from leaving the screen:

    void Update ()
    {
        Vector3 pos = this.transform.position;
       
        if (Input.GetKey(KeyCode.Space))
            Instantiate(bullet, this.transform.position, Quaternion.identity);
        if (Input.GetKey(KeyCode.LeftArrow) && pos.x > -2.5f)
            this.transform.position = new Vector3(pos.x - 0.1f, pos.y, pos.z);
        if (Input.GetKey(KeyCode.RightArrow) && pos.x < 2.5f)
            this.transform.position = new Vector3(pos.x + 0.1f, pos.y, pos.z);
    }

Cooldown

As it is, the Player can fire as many bullets as it likes, with no gap between one and another. In order to implement a cooldown, we use an approach similar to this. When a bullet is fired, the time is recorded and no bullet may be fired before the cooldown period has passed. To do this, declare the following variables in the Player class:

    public float cooldown = 1.0f; // minimum time between bullets
    private float nextFire = 1.0f; // next time when a bullet can be fired

Then, adjust the firing code as follows:

        if (Input.GetKey(KeyCode.Space) && Time.time >= nextFire)
        {
            nextFire += cooldown;
            Instantiate(bullet, this.transform.position, Quaternion.identity);
        }

Then, try it out, and adjust cooldown as you like:


Then, rejoice.


Alien Movement

In Space Invaders, aliens move right, down, left, down, etc. Making this work properly takes a bit of work, so I'll just give you the code:

public class Alien : MonoBehaviour
{
    public float horDistance = 1.0f;
    public float verDistance = 0.5f;
    public float speed = 0.4f;
    private float startTime;
    private Vector3 startingPosition;
    private Vector3 target;
   
    // Use this for initialization
    void Start ()
    {
        this.startTime = Time.time;
        this.startingPosition = this.transform.position;
        this.target = this.startingPosition + new Vector3(this.horDistance, 0, 0);
    }
   
    // Update is called once per frame
    void Update ()
    {
        Vector3 currentPosition = this.transform.position;
        Vector3 newPosition = Vector3.MoveTowards(currentPosition, this.target, speed * Time.deltaTime);
        this.transform.position = newPosition;
       
        //print (this.target);
       
        if (newPosition == target)
        {
            if (newPosition.x == startingPosition.x)
            {
                if (((newPosition.y - startingPosition.y) / verDistance) % 2 == 0)
                    this.target = newPosition + new Vector3(this.horDistance, 0, 0);
                else
                    this.target = newPosition - new Vector3(0, this.verDistance, 0);
            }
            else
            {
                if (((newPosition.y - startingPosition.y) / verDistance) % 2 != 0)
                    this.target = newPosition - new Vector3(this.horDistance, 0, 0);
                else
                    this.target = newPosition - new Vector3(0, this.verDistance, 0);              
            }
        }
    }
}

Don't fret about what the code is doing... just use it. Tweak the public variables as you like.

More Aliens

Since we need a lot of aliens, it's a good idea to make the Alien object a prefab. To do this, drag it from the Hierarchy panel to the Assets panel. If you did it right, its listing in the Hierarchy panel should turn blue. This way, if you make any changes, you can apply them across all Aliens.

To make multiple Aliens, you can either press Ctrl+D (duplicate) to create additional objects, or else instantiate all the aliens in code using for loops. In my case, I just duplicated a handful of Aliens, just for testing:


Don't get too fussy about the Alien placement and all that. The reason we're still working with cubes and spheres is that you should first concentrate on making sure that your game mechanics work correctly. Only once that is done should you spend some time (a lot of time, actually) making it look good.

Aliens Shooting

Allowing Aliens to shoot the Player is no different from allowing the Player to shoot the Aliens. However, you now need a different bullet that moves downwards and that can destroy the player.

Once you attach a rigidbody to the Player (Component -> Physics -> Rigidbody, and remember to turn off "Use Gravity"), you will find some unexpected behaviour: at times, when the Player shoots a bullet, the Player itself is destroyed! That's because we don't yet have any code in the Bullet script that distinguishes between objects it can destroy. So far it worked correctly only because the Player didn't have a rigidbody, in which case the collision doesn't happen.

An easy way to solve this is to tag the object. Select the Player. At the top of the Inspector is a section where you can select a tag to assign to the object. For the Player, you can choose the predefined Player tag:


For the Aliens, you will need to add a new tag. From the drop-down list containing tags (shown above), select "Add Tag...".


In the resulting interface (shown above), expand the "Tags" node. Next to "Element 0", type "Alien". This creates the "Alien" tag, but does not assign it to the object.


To tag the Alien as such, select an Alien object, and select the newly available "Alien" tag from the drop-down list. Remember to click "Apply" to make this work for all Alien objects (since it's a prefab).

The Bullet script's collision code now becomes:

    void OnCollisionEnter(Collision collision)
    {
        if (collision.gameObject.tag == "Alien")
        {
            GameObject.Destroy(collision.gameObject);
            GameObject.Destroy(this.gameObject);
        }
    }

The Player's bullets should now destroy only Aliens.

For the aliens to shoot at the player, you will first need to create another bullet prefab (e.g. "AlienBullet") with the same script as the Bullet, except for two key differences. First, the bullet needs to go downwards instead of upwards. So:

    void Start ()
    {
        this.target = this.transform.position - new Vector3(0, 20, 0);
    }

Secondly, it needs to collide with the Player instead of the Alien:

void OnCollisionEnter(Collision collision)
{
if (collision.gameObject.tag == "Player")
{
GameObject.Destroy(collision.gameObject);
GameObject.Destroy(this.gameObject);
}
}


Finally, the code for shooting (in the Alien script) will be a little different from that of the Player. Instead of shooting when the user presses a key, the Alien will shoot with a random delay. First, declare the following variables in the Alien class:

    public GameObject bullet;
    public float minShootDelay = 1.0f;
    public float maxShootDelay = 7.0f;
    private float nextShootTime = 0.0f;

In the Unity editor, set the AlienBullet prefab in the Alien script's bullet slot.

In the Start() method, add code to initialise nextShootTime:

        this.nextShootTime = Random.Range(minShootDelay, maxShootDelay);

Unity's Random.Range() method is similar to .NET's Random.Next() method, but not quite the same.

Change the code that handles when bullets leave the game area, since these bullets are going down instead of up:

        if (this.transform.position.y < -3.0f)
            GameObject.Destroy(this.gameObject);

Finally, in the Update() method, we add code for actual shooting:

        if (Time.time > nextShootTime)
        {
            Instantiate(bullet, this.transform.position, Quaternion.identity);
            nextShootTime = Time.time + Random.Range(minShootDelay, maxShootDelay);
        }

If you try it now, things will get a little bit messy as the Aliens' bullets will collide with themselves:


Explaining how to solve this requires an article in itself. Notwithstanding this, in this (long) article we have taken care of several behaviour issues with both the Player and the Aliens, and brought the game much closer to a working Space Invaders clone.

This is the last article in the Ranch Invaders series. I hope you'll return to the Ranch to read about other interesting topics! :)

Thursday, May 30, 2013

Unity3D: Space Invaders (Part 3 - Prefabs)

Hello, and welcome back to the Ranch! :)

This article continues with the Ranch Invaders series. However, like with yesterday's article, if you haven't read the previous articles, you can simply follow along by creating a new Unity project.

In today's article, we're going to add the player's ship into the game, and allow it to shoot bullets. In doing this, we will learn what prefabs are, and how to create objects while the game is running.

From the GameObject menu, go to Create Other -> Sphere. In the Hierarchy section, select the Sphere, press F2, and rename it to "Player". Move it so that it appears in the bottom of the camera's view (you might want to select the Main Camera to see its preview while you do this).

Next, from the GameObject menu, go to Create Other -> Capsule. Rename it to "Bullet". After pressing the 'R' key to go into scaling mode, resize the bullet (capsule) so that it is much smaller than the player (sphere).

Your work so far might look something like this:



Now, from the Hierarchy panel, drag the Bullet into the Assets panel:


The result is that the Bullet is added to the Assets panel, and in the Hierarchy panel, its entry turns blue. This means that the Bullet is now a prefab.

The Player and the Alien objects are simply objects in the scene. Since the Bullet is a prefab, we can refer to it in scripts and create as many Bullets as we like through code. A great thing about prefabs is that you can customise the prefab once (e.g. make a bullet with a yellow material), and all instances of that prefab will have the same features. Let's see how this works in practice.

In the Assets panel, right click somewhere, and in the context menu, select Create -> Material. Name it "Bullet". In the Main Color property, select whichever colour you think is best for a bullet (e.g. yellow). Drag the material onto the Bullet object either in the Hierarchy panel or in the world view:

Right click the Assets panel and Create -> C# Script. Name it "Bullet". Drag the script onto the Bullet object. Double-click the script to launch MonoDevelop.

In the Bullet class, add a public variable to represent the speed:

    public float speed = 1.0f;

When you expose a public variable like this, it appears in the Inspector in the section where the script is:


You can now change this variable as you like through the Inspector, and when you Play the game in Unity, that value will be used. Note, however, that if you change such variables while Playing, changes will be reverted once you stop playing.

Now, we need to write a script that will take care of the bullet's movement. Since we're doing Sp-- sorry, Ranch Invaders, we're looking to do something like this:


The bullet starts at the player's position, and is shot upwards where the Aliens will be. In code, we can do this as follows:

public class Bullet : MonoBehaviour
{
    public float speed = 1.0f;
    private Vector3 target;
   
    // Use this for initialization
    void Start ()
    {
        this.target = this.transform.position + new Vector3(0, 20, 0);
    }
   
    // Update is called once per frame
    void Update ()
    {
        Vector3 currentPosition = this.transform.position;
        Vector3 newPosition = Vector3.MoveTowards(currentPosition, this.target, speed * Time.deltaTime);
        this.transform.position = newPosition;
    }
}

Remember that we're going to have the Player create (shoot) bullets. When the bullets are created, they will be at the Player's position. We want the bullets to go upwards, so we define a target that is a certain distance away (e.g. 20 units). By adding a Vector3(0, 20, 0) that simply points directly upwards, to the initial bullet position, we can set a destination for the bullet that is directly above its starting position.

In the Update() method, we use Vector3.MoveTowards() which works pretty much like Lerp (see yesterday's article). Except that instead, we are moving the Bullet from its current position towards the target in steps of speed * Time.deltaTime. Time.deltaTime is a measure of frame time, so the bullet will move a short length upwards with every frame. We multiply speed to be able to control the speed.

From the Unity editor, press the Play button and watch the bullet move upwards. If you think it's too fast or too slow, tweak the speed by selecting the Bullet and changing the speed value in the Inspector.

You can now delete the Bullet from the scene. Since it's among our Assets, we can create it at runtime. Create a new C# script called "Player" and drag it onto the Player object. Double-click the Player script to open it in MonoDevelop.

In the Player class, add the following public variable:

    public GameObject bullet;

Save the script. Back in Unity, select the Player object, and in the Inspector, under the Player Script, notice that there is now a slot for the bullet game object:


From the Assets panel, drag the Bullet prefab into the appropriate slot in the Inspector, where it currently says "None (Game Object)".

Now, from the Player script, we can make instances of this prefab. In the Update() method of the Player script, just add the following:

        if (Input.GetKeyDown(KeyCode.Space))
        {
            Instantiate(bullet, this.transform.position, Quaternion.identity);
        }

So all we're doing here is that when the player presses the Space key, we use the Instantiate() method to create a bullet. We give it the object we want to instantiate (in this case the bullet prefab), and its starting position and orientation (rotation). I'm not going to go into quaternions here, but Quaternion.identity simply means that the bullet isn't rotated.

You can now press Play to try out the game. Press Space to watch bullets move like sausages in the sunset:


If it doesn't work, don't worry! :) Depending on how you carried out the steps above, it is possible that the bullet prefab wasn't saved correctly. In my case, for example, I found that the Bullet script was missing. Since a prefab is really just a template for an object, you need to make sure that any changes to it apply to all the objects that use that template. So in the case of the missing script, once you drag the Bullet script onto a Bullet object in the scene, you need to click "Apply" at the top of the Inspector in order to apply the changes to all affected objects.

Great! We can now shoot sausage-like bullets to our heart's content. We still have some work left to do, though: we need more aliens, the player's ship needs to move sideways, bullets need to destroy aliens, etc. We'll handle some of these things in the next article. Until then, au revoir! :)