Project Practice #1: Asteroids (Days 1 & 2 )

I’d like to start this post with an admission.

I’m not starting my game development journey from scratch here. Though I might as well be. I have done a few minor game jam projects in the past but I’m going on my own now and I’ve been on hiatus for a long while. This blog is not written for the absolute beginners in mind but feel free to use any code I share here. I hope you have an approximation of knowledge similar to where I am when I start a new project which at this stage means extremely basic coding knowledge and similar unity understanding.

As I say.. I’m not as far along in my progress as I’d like to be and a good friend of mine has decided she wants to learn Unity as well so together we are recreating small arcade games to get into the swing of development for our own projects. We both have a lot of ideas we want to get out there but currently lack the skillset to achieve them. This is where we are starting and I’ll be updating you along the way!


Day 1: Movement and Input

This one was probably obvious to many of you and you might have done it a lot smarter than we did but here we go.

For context my friend Missie is working on a game inspired by Lunar Lander so I was teaching her about the Unity physics engine which means that’s what we’ll be using for movement in this game. There are probably better options but we wanted to make sure we could do physics related shenanigan’s if we wanted to. The full code will be available on my github.

We will also be using the new input system here so if you’re not familiar with that one be sure to look into it because in most cases that’s what we will be using here. If you are curious what my input mapping looks like I’ve included it below and we set the behavior for the Player Input component to Unity Events.

You could probably combine the “Rotate” action and the “Move” Action but it seemed more complicated in code to me right now.

If you’re building along I should point out you need a Rigidybody2D as we are working in the 2D Universal Render Pipeline and moving our ship using the built in physics engine. Don’t forget to set the Gravity scale to 0 if you’re not actually changing the gravity to 0 in the project settings. Additionally your Linear Drag and Angular Drag will need to be tweaked depending on the size of your ship. I’m currently using a 16×16 pixel ship set to scale 4.

At this point we decided to focus purely on movement. Firing the gun(s) would come later. The following code is what we came up with.

Remember: when using the new input system you need to include the namespace “UnityEngine.InputSystem”.

ShipController.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.InputSystem;

public class ShipController : MonoBehaviour
{
    [SerializeField] private Rigidbody2D shipRB;
    [SerializeField] private float thrustAmount;
    [SerializeField] private bool thrusting;
    [SerializeField] private float thrustDirection, rotSpeed;
    private float rot;
    

    public void Thrust(InputAction.CallbackContext context)
    {
        if (context.performed) //When the key is pressed
        {
            thrusting = true;
        }
        if (context.canceled) //When the key is released
        {
            thrusting = false;
        }
    }

    public void MoveShip(InputAction.CallbackContext context)
    {
        if (context.performed)
        {
            thrustDirection = context.ReadValue<Vector2>().y;
            thrusting = true;
        }
        if (context.canceled)
        {
            thrustDirection = 0;
            thrusting = false;

        }
    }


    public void RotateShip(InputAction.CallbackContext context)
    {
        if (context.performed)
        {
            rot = context.ReadValue<Vector2>().x;
        }
        if (context.canceled)
        {
            rot = 0;
        }
    }

    
    public void FixedUpdate()
    {

        shipRB.AddTorque(-rot * rotSpeed,ForceMode2D.Force);

        if (thrusting)
        {
            shipRB.AddForce((gameObject.transform.up * thrustAmount)* thrustDirection, ForceMode2D.Force);
        }
        else
        {
            shipRB.velocity = new Vector2(shipRB.velocity.x, shipRB.velocity.y);
            
        }
    }
}

I almost forgot: Don’t forget to assign the functions to events in the unity events on the Player Input component as such for every action function.

That is where we left off on day one. Of course we also added a basic ship sprite and game object to add these to as well. Since one of us is learning Unity still it was decided not to overload ourselves with too many concepts at once and just go at our own pace


Day 2: Screen Wrapping and Shooting

The next day was a bit more of a technical day for us. We didn’t get as much done as we would have liked but we did manage to leave nothing half completed at least.

In many old arcade games like Asteroids the player character could wrap from one side of the map to the other as if the edges of the playable area were wormholes.. something we needed to replicate. We both set off to figure out the best solution but ultimately Missie’s was the better one. Instead of my idea where we would put collision boxes outside the play area and teleport the ship OnTriggerEnter(), her idea was to convert the camera space to world space values. Turns out this was actually way easier than I expected.

There’s likely better ways to go about doing this but like I say this isn’t as much a tutorial as it is my journal for how things go down. First we created a script to determine the size of the screen and store it as a variable both at start and at runtime just in case the resolution changes on the fly. We figure there will only be one instance of this code running so we could go ahead and make it some kind of singleton instance.

Boundries.cs
using UnityEngine;

public class Boundries : MonoBehaviour
{
    public static Boundries Instance;
    public Vector2 screenBounds;
    [SerializeField] private Camera cam;

    private void Awake()
    {
        Instance = this;
    }
    void Start()
    {
        screenBounds = cam.ScreenToWorldPoint(new Vector3(Screen.width, Screen.height, cam.transform.position.z));
    }
    private void Update()
    {
        screenBounds = cam.ScreenToWorldPoint(new Vector3(Screen.width, Screen.height, cam.transform.position.z));
        
    }  
}

Next we needed a script to slap onto anything that we wanted to be able to wrap back around the screen. Things such as projectiles, the ship, and the asteroids themselves.

Wrapper.cs
using UnityEngine;

public class Wrapper : MonoBehaviour
{
    [SerializeField] private Vector3 pos;
    [SerializeField] private float offset = 0.5f;
    private void Update()
    {
        pos = gameObject.transform.position;
    }
    void LateUpdate()
    {
        Vector3 bounds = Boundries.Instance.screenBounds;
        
        if (pos.x >= bounds.x + offset)
        {
            gameObject.transform.position = new Vector3(-bounds.x, pos.y);
        }
        if (pos.x <= -bounds.x - offset)
        {
            gameObject.transform.position = new Vector3(bounds.x, pos.y);
        }
        if (pos.y <= -bounds.y - offset)
        {
            gameObject.transform.position = new Vector3(pos.x, bounds.y);
        }
        if (pos.y >= bounds.y + offset)
        {
            gameObject.transform.position = new Vector3(pos.x, -bounds.y);
        }


    }
}

Simple! Now it was time to handle firing the gun.

It’s actually surprising how often shooting something appears in a video game. It’s quite a shame really.. but that’s besides the point. This is thankfully a rather simple task. Since I’ll be using the Instantiate() method it comes with all the parameters I need to make a simple bullet. First however I needed an actual bullet and a place to shoot from. Easily enough I just made a square sprite which I shrank to about the size of the front of the ship and stretched to be longer. Next I threw a new script on there called Projectile.cs and made the whole thing a prefab.

Projectile.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Projectile : MonoBehaviour
{
    private Rigidbody2D projectileRB;
    [SerializeField] private int speed;
    [SerializeField] private float timeToLive;
    
    private void Awake()
    {
        projectileRB = GetComponent<Rigidbody2D>();
    }
    // Start is called before the first frame update
    void Start()
    {
        projectileRB.AddForce(gameObject.transform.up * speed, ForceMode2D.Impulse);

    }

    // Update is called once per frame
    void Update()
    {
        timeToLive -= Time.deltaTime;
        if(timeToLive <= 0.0f)
        {
            Destroy(gameObject);
        }
    }
}

Obviously this bit of code doesn’t do much on its own.. it just moves the projectile when it’s instantiated. The next step would of course be to edit the ship controller and the ship game object. Since the instantiate function has a parameter for location and rotation we can use an empty game object placed in front of the ship to easily set those in the editor without much fancy math, plus we can move it if we need to later very easily.

Make sure the green axis arrow is pointing forward relative to your ship.

ShipController.cs

Variables

    [Header("Projectile Info")]
    [SerializeField] private bool firing;
    [SerializeField] private float fireRate;
    [SerializeField] private bool allowFire = true;
    [SerializeField] private Projectile projectile;
    [SerializeField] private Transform gun;

New Functions

public void Fire(InputAction.CallbackContext context)
    {
        if (context.performed)
        {
            firing = true;
        }
        if (context.canceled)
        {
            firing = false;
        }
    }
public void ShootGun()
    {
        if(allowFire && firing)
        {
            StartCoroutine(FireRate());
        }

        IEnumerator FireRate()
        {
            allowFire = false;
            
            Instantiate(projectile, gun.transform.position, gun.transform.rotation);
            yield return new WaitForSeconds(fireRate);
            allowFire = true;
        }
    }
public void Update()
    {
        ShootGun();
   
    }
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.InputSystem;

public class ShipController : MonoBehaviour
{
    [SerializeField] private Rigidbody2D shipRB;
    [SerializeField] private float thrustAmount;
    [SerializeField] private bool thrusting;
    [SerializeField] private float thrustDirection, rotSpeed;
    [SerializeField] private ParticleSystem particles;


    [Header("Projectile Info")]
    [SerializeField] private bool firing;
    [SerializeField] private float fireRate;
    [SerializeField] private bool allowFire = true;
    [SerializeField] private Projectile projectile;
    [SerializeField] private Transform gun;
    private float rot;
    

    public void Thrust(InputAction.CallbackContext context)
    {
        if (context.performed)
        {
            thrusting = true;
        }
        if (context.canceled)
        {
            thrusting = false;
        }
    }

    public void MoveShip(InputAction.CallbackContext context)
    {
        if (context.performed)
        {
            thrustDirection = context.ReadValue<Vector2>().y;
            thrusting = true;
        }
        if (context.canceled)
        {
            thrustDirection = 0;
            thrusting = false;

        }
    }


    public void RotateShip(InputAction.CallbackContext context)
    {
        if (context.performed)
        {
            rot = context.ReadValue<Vector2>().x;
        }
        if (context.canceled)
        {
            rot = 0;
        }
    }

    public void Fire(InputAction.CallbackContext context)
    {
        if (context.performed)
        {
            firing = true;
        }
        if (context.canceled)
        {
            firing = false;
        }

    }

    public void Update()
    {

        ShootGun();
        
    }
    public void FixedUpdate()
    {

        shipRB.AddTorque(-rot * rotSpeed,ForceMode2D.Force);

        if (thrusting)
        {
            shipRB.AddForce((gameObject.transform.up * thrustAmount)* thrustDirection, ForceMode2D.Force);
            
        }
        else
        {
            shipRB.velocity = new Vector2(shipRB.velocity.x, shipRB.velocity.y);
            
        }
    }

    public void ShootGun()
    {
        if(allowFire && firing)
        {
            StartCoroutine(FireRate());
        }

        IEnumerator FireRate()
        {
            allowFire = false;
            
            Instantiate(projectile, gun.transform.position, gun.transform.rotation);
            yield return new WaitForSeconds(fireRate);
            allowFire = true;
        }
    }
}

It’s important to remember to set values in the inspector for things like Fire Rate and the gun GameObject.

And there we go! Now throw Wrapper.cs onto the projectile prefab, save it, and test it out! In Part 2 I will cover days 3 & 4 which will include: particle systems, ui, asteroid spawning, bullet hits, score, and more!

Leave a Reply

Your email address will not be published.