How to build an advanced space shooter in Unity3D

Followed this official tutorial on how to build an advanced space shooter in Unity3D, my own version on GitHub, and you can see it in action here (WSAD to move, mouse click to shoot).

  1. Create a new 2D project, download and import the assets from https://www.assetstore.unity3d.com/en/#!/content/13866
  2. Save a scene in _Scenes folder and name it Main
  3. File -> Build settings -> Web player -> Switch platform
  4. Player Settings… – define the size
  5. Reorder the layout the way you want but remember to save it by clicking the Layout button in the top right corner
  6. Drag Vehicle_play from the Assets->Model folder to the Hierarchy
    1. Rename to player, reset transform origin
    2. Add component -> Physics -> Rigidbody and deselect Use Gravity
    3. Add component -> Physics -> Capsule collider, change direction to Z-Axis and adjust position of X, and check Is Trigger
    4. Click on the y gizmo of the camera and adjust again
    5. In this example the Capsule collider would suffice, but we can add Mesh collider and this is very detailed but we can also drag a predefined Mesh collider from Models folder to Mesh collider -> Mesh variable which is less detailed but sufficent and better than the Capsule collider
      1. To see the mesh collider turn off the Mesh renderer
    6. Add Prefabs -> VFX -> Engines -> engines_player to the player object
  7. Reset camera transform origin
    1. set transform Y to 10, Z to 5 (so that the ship starts at the bottom)
    2. set rotation X to 90
    3. Set Projection to ortographic
    4. Set Size to 10
    5. Set Clear flags to Solid Color
    6. Background black
  8. Edit -> Render settings – property Ambient light set to Black (0,0,0,255) to effectively turn it off
  9. Lights
    1. Main light
      1. Hierarchy -> Create -> Directional light (based on rotation!, not position) and rename accordingly
      2. Reset origin position
      3. Set Rotation X = 20, Y = – 115
      4. Intensity = 0.75
    2. Fill light
      1. Duplicate the Main light and rename accordingly
      2. Rename to Fill light
      3. Reset its rotation only
      4. Set Rotation Y = 115
      5. Set Color to some shade of blue
    3. Rim light
      1. Duplicate the Fill light and rename accordingly
      2. Reset origin position
      3. Color White
      4. Rotation Y = 65, X = -15
      5. Intensity = 0.25
  10. Object organization
      1. Create new empty object (ctrl + shift + n)
      2. Rename to Lights
      3. Rest transform origins
      4. Move the whole Lights object out of the way by setting the position Y = 100
  11. Background
    1. Hierarchy -> Create -> 3D object -> Quad; rename accordingly
    2. Reset transform origin
    3. Set rotation X = 90
    4. Remove the Mesh Collider component
    5. Drag Assets -> Textures -> tile_nebula_green to the Quad object
    6. Scale X = 15, Y = 30
    7. Shaders; difuse – mat, specular – glossy, unlit -> texture is the one we will use since this makes it independent of the lighting system (it displays the image originally as it looks)
    8. Position Y = -10 because it otherwise overlaps with 0,0,0 player object position
  12. Moving the player ship
    1. In the Assets folder create folder Scripts
    2. Click on the player and Add Component -> Script -> name it PlayerController
    3. Drag the created script file to the Scripts folder
    4. You have Update and FixedUpdate. We will use since we are dealing with physics.
    5. #pragma strict
      public var speed = 12;
      
      function FixedUpdate () {
      	var moveHorizontal = Input.GetAxis("Horizontal") ;
      	var moveVertical = Input.GetAxis("Vertical");
      	
      	rigidbody.velocity = Vector3(moveHorizontal, 0, moveVertical) * speed;	
      }
    6. Input.GetAxis returns a number from 0 to 1 that’s why you have to multiply
    7. At this point the player ship can go out of the play area, to fix this, use the Mathf.Clamp function:
      #pragma strict
      
      public class Boundary extends System.Object {
      	public var zMin : float;
      	public var zMax : float;
      	public var xMin : float;
      	public var xMax : float;
      }
      
      public var speed = 10;
      public var boundary : Boundary; 
      
      function FixedUpdate () {
      	var moveHorizontal = Input.GetAxis("Horizontal") ;
      	var moveVertical = Input.GetAxis("Vertical");
      	
      	rigidbody.velocity = Vector3(moveHorizontal, 0, moveVertical) * speed;	
      	
      	rigidbody.position = Vector3(
      		Mathf.Clamp(rigidbody.position.x, boundary.xMin, boundary.xMax),
      		0,
      		Mathf.Clamp(rigidbody.position.x, boundary.zMin, boundary.zMax)
      	);
      }
    8. extends System.Object is used to Serialize the object to be able to be used within other object and visible in the inspector
    9. To add tilting when moving:
      #pragma strict
      
      public class Boundary extends System.Object {
      	public var zMin : float;
      	public var zMax : float;
      	public var xMin : float;
      	public var xMax : float;
      }
      
      public var speed = 10;
      public var boundary : Boundary; 
      public var tilt = 4;
      
      function FixedUpdate () {
      	var moveHorizontal = Input.GetAxis("Horizontal") ;
      	var moveVertical = Input.GetAxis("Vertical");
      	
      	rigidbody.velocity = Vector3(moveHorizontal, 0, moveVertical) * speed;	
      	
      	rigidbody.position = Vector3(
      		Mathf.Clamp(rigidbody.position.x, boundary.xMin, boundary.xMax),
      		0,
      		Mathf.Clamp(rigidbody.position.z, boundary.zMin, boundary.zMax)
      	);
      	
      	rigidbody.rotation = Quaternion.Euler(0, 0, rigidbody.velocity.x * - tilt);
      }
  13. Bullets
    1. Create a new empty object rename to Bolt
    2. Reset transform
    3. Create Quad, reset transform, rename VFX
    4. Add VFX as Child of the Bolt
    5. Rotate X = 90
    6. Drag Textures -> fx_lazer_orange_dff to it
    7. Shader -> Particles -> Additive (you can also use Mobile version)
    8. Add component -> Physics -> Rigidbody, deselect Use gravity
    9. Remove the VFX Mesh collider
    10. Select Bolt and add Capsule colider
    11. Change radius and Direction to Z-axis
    12. Check the Is Trigger checkbox
    13. Add script to Bolt object, name it Mover
    14. Make it a Prefab
    15. Delete it from Hierarchy
  14. Firing Bullets
    1. PlayerController.js:
      #pragma strict
      
      public class Boundary extends System.Object {
      	public var zMin : float;
      	public var zMax : float;
      	public var xMin : float;
      	public var xMax : float;
      }
      
      public var bullet : GameObject;
      
      public var speed = 10;
      public var boundary : Boundary; 
      public var tilt = 4;
      public var waitPeriod = 0.05;
      private var nextFire : float;
      
      function FixedUpdate () {
      	var moveHorizontal = Input.GetAxis("Horizontal") ;
      	var moveVertical = Input.GetAxis("Vertical");
      	
      	rigidbody.velocity = Vector3(moveHorizontal, 0, moveVertical) * speed;	
      	
      	rigidbody.position = Vector3(
      		Mathf.Clamp(rigidbody.position.x, boundary.xMin, boundary.xMax),
      		0,
      		Mathf.Clamp(rigidbody.position.z, boundary.zMin, boundary.zMax)
      	);
      	
      	rigidbody.rotation = Quaternion.Euler(0, 0, rigidbody.velocity.x * - tilt);
      }
      
      function Update(){
      	if (Input.GetButton("Fire1") && Time.time > nextFire){
      		nextFire = Time.time + waitPeriod;
      		Instantiate(bullet, Vector3(rigidbody.position.x, 0, rigidbody.position.z), Quaternion.identity);
      	}
      }
    2. Drag the bullet prefab to the bullet variable on the player script
  15. Cleaning up
    1. Create Cube, rename Boundary
    2. Reset transform origin and place it around the whole game
    3. Turn off Mesh Renderer
    4. Is Trigger on Box collider
    5. Remove the renderer
    6. Add script Destroy:

      function OnTriggerExit(other : Collider)
      {
          Destroy(other.gameObject);
      }
  16. Enemies
    1. Create an empty game object – Asteroids
    2. Reset transform origin, move a bit away along the Z axis
    3. Drag the Asteroid model from the Models folder and place it as a child of the Asteroids empty game object – RTO (reset transform origin from now on)
    4. Click on the EGO (empty game object) and add Physics -> Rigidbody
    5. Deselect Use Gravity
    6. Add -> Physics -> Capsule collider
    7. Add Script:
      public var tumble : float;
      
      function Start () {
      	rigidbody.angularVelocity = Random.insideUnitSphere * tumble;
      }
    8. AngularVelocity – how fast the object is rotating
    9. Remove the AngularDrag which eventually slows the asteroid to a halt (drag – trenje for all you Croatian readers)
    10. Add script DestroyByContact:
      #pragma strict
      
      function OnTriggerEnter (other : Collider) {
      	if (other.tag == "Boundary"){
      		return;
      	}
      		
      	Destroy(other.gameObject);
      	Destroy(gameObject);
      }
    11. Add Tag Boundary to Boundary
  17. Explosions
    1.  Adjust the DestroyByContact script:
      #pragma strict
      
      public var asteroidExplosion : GameObject;
      public var playerExplosion : GameObject;
      
      function OnTriggerEnter (other : Collider) {
      	if (other.tag == "Boundary"){
      		return;
      	}
      	
      	//it will create an explosion on the same position as our asteroid
      	Instantiate(asteroidExplosion, transform.position, transform.rotation);
      	
      	if (other.tag == "Player"){
      		Instantiate(playerExplosion, other.transform.position, other.transform.rotation);
      	}
      	
      	Destroy(other.gameObject);
      	Destroy(gameObject);
      }
    2. Drag the needed explosions from the VFX folder
    3. Drag the Mover script to the Asteroid and set speed to -5
    4. Drag Asteroid to a Prefab folder
    5. Delete it from Hierarchy
  18. Game Controller
    1. Create EGO and rename accordingly
    2. Set tag to GameController
    3. Add script:
      #pragma strict
      
      public var enemy : GameObject;
      public var spawnValues : Vector3;
      
      
      function Start () {
      	SpawnWaves();
      }
      
      function SpawnWaves(){
      	
      	var spawnPosition = Vector3(Random.Range(-spawnValues.x, spawnValues.x), spawnValues.y, spawnValues.z);
      	var spawnRotation = Quaternion.identity;
      	
      	Instantiate(enemy, spawnPosition, spawnRotation);
      }
    4. Outside set values for spawnValues to 6, 0, 18
  19. Add wave of enemies
    #pragma strict
    
    public var enemy : GameObject;
    public var spawnValues : Vector3;
    public var enemyCount : int;
    public var waitForEnemy : float;
    public var waitForPlayer : float;
    public var waveWait : float;
    
    function Start () {
    	SpawnWaves();
    }
    
    function SpawnWaves(){
    	yield WaitForSeconds(waitForPlayer);
    	
    	while (true){
    		for (var i=0; i<enemyCount; i++){
    			var spawnPosition = Vector3(Random.Range(-spawnValues.x, spawnValues.x), spawnValues.y, spawnValues.z);
    			var spawnRotation = Quaternion.identity;
    		
    			Instantiate(enemy, spawnPosition, spawnRotation);
    			yield WaitForSeconds(waitForEnemy);
    		}
    	}
    	
    	yield WaitForSeconds(waveWait);	
    }
  20. Destroying explosion animations
    1. Add new script DestroyByTime

      #pragma strict
      
      public var aliveTime : float;
      function Start () {
      	Destroy(gameObject, aliveTime);
      }
    2. Set this script on all of the explosion prefabs
    3. Set AliveTime to 2
  21. Audio
    1. Drag the Audio files to appropriate Explosion Prefabs
    2. For a weapon_player – drag it to a Player object in Hierarchy and remove the Play on Awake
    3. Update the PlayerController script to:
      function Update(){
      	if (Input.GetButton("Fire1") && Time.time > nextFire){
      		nextFire = Time.time + waitPeriod;
      		Instantiate(bullet, Vector3(rigidbody.position.x, 0, rigidbody.position.z), Quaternion.identity);
      		
      		audio.Play();
      	}
      }
    4. Drag music_background to GameController
    5. Select Loop and Play on Awake
    6. Adjust the volumes
      1. Player 0.5
      2. Game Controller 0.5
  22. Displaying score
    1. Create -> UI -> Text, rename to ScoreText
    2. Set correct Rect Transform
    3. In the GameController script add:
      #pragma strict
      
      public var enemy : GameObject;
      public var spawnValues : Vector3;
      public var enemyCount : int;
      public var waitForEnemy : float;
      public var waitForPlayer : float;
      public var waveWait : float;
      
      public var scoreText : UnityEngine.UI.Text;
      private var score : int;
      
      function Start () {
      	score = 0;
      	updateScore();
      	SpawnWaves();
      }
      
      function SpawnWaves(){
      	yield WaitForSeconds(waitForPlayer);
      	
      	while (true){
      		for (var i=0; i<enemyCount; i++){
      			var spawnPosition = Vector3(Random.Range(-spawnValues.x, spawnValues.x), spawnValues.y, spawnValues.z);
      			var spawnRotation = Quaternion.identity;
      		
      			Instantiate(enemy, spawnPosition, spawnRotation);
      			yield WaitForSeconds(waitForEnemy);
      		}
      	}
      	
      	yield WaitForSeconds(waveWait);	
      }
      
      function updateScore(){
      	scoreText.text = "Score: " + score;
      }
      
      function IncreaseCount(){
      	score ++;
      	updateScore();
      }
    4. In the DestroyByContact:
      #pragma strict
      
      public var asteroidExplosion : GameObject;
      public var playerExplosion : GameObject;
      private var gameController : GameController;
      
      function Start(){
      	var gameControllerObject : GameObject = GameObject.FindWithTag("GameController");
      	if (gameControllerObject != null){
      		gameController = gameControllerObject.GetComponent(GameController);
      	}
      	else{
      		Debug.Log("oops");
      	}
      }
      
      function OnTriggerEnter (other : Collider) {
      	if (other.tag == "Boundary"){
      		return;
      	}
      	
      	//it will create an explosion on the same position as our asteroid
      	Instantiate(asteroidExplosion, transform.position, transform.rotation);
      	
      	if (other.tag == "Player"){
      		Instantiate(playerExplosion, other.transform.position, other.transform.rotation);
      	}
      	
      	gameController.IncreaseCount();
      	
      	Destroy(other.gameObject);
      	Destroy(gameObject);
      }
    5. Drag the ScoreText object to form the reference to it in the GameController
  23. Ending the game
    1. Create an empty game object and rename it to DisplayText
    2. Rest transform origin
    3. Add ScoreText to it (drag the Canvas containing it)
    4. Create new UI -> Text (rename to RestartText), and place it in the upper right corner
    5. Create new UI -> Text (rename to GameOverText)
    6. Update the script to:
      #pragma strict
      
      public var enemy : GameObject;
      public var spawnValues : Vector3;
      public var enemyCount : int;
      public var waitForEnemy : float;
      public var waitForPlayer : float;
      public var waveWait : float;
      
      public var scoreText : UnityEngine.UI.Text;
      public var restartText : UnityEngine.UI.Text;
      public var gameOverText : UnityEngine.UI.Text;
      
      private var restart : boolean;
      private var gameOver : boolean;
      
      private var score : int;
      
      function Start () {
      	restart = false;
      	gameOver = false;
      	
      	restartText.text = "";
      	gameOverText.text = "";
      	
      	score = 0;
      	updateScore();
      	SpawnWaves();
      }
      
      function Update(){
      	if (restart){
      		if (Input.GetKeyDown(KeyCode.R)){
      			Application.LoadLevel(Application.loadedLevel);
      		}
      	}
      }
      
      function SpawnWaves(){
      	yield WaitForSeconds(waitForPlayer);
      	
      	while (true){
      		for (var i=0; i<enemyCount; i++){
      			var spawnPosition = Vector3(Random.Range(-spawnValues.x, spawnValues.x), spawnValues.y, spawnValues.z);
      			var spawnRotation = Quaternion.identity;
      		
      			Instantiate(enemy, spawnPosition, spawnRotation);
      			yield WaitForSeconds(waitForEnemy);
      		}
      		
      		if (gameOver){
      			restartText.text = "R to restart";
      			restart = true;
      			break;
      		}
      	}
      	
      	yield WaitForSeconds(waveWait);	
      }
      
      function updateScore(){
      	scoreText.text = "Score: " + score;
      }
      
      function IncreaseCount(){
      	score ++;
      	updateScore();
      }
      
      function GameOver(){
      	gameOver = true;
      	gameOverText.text = "Game over!";
      }
    7. Drag the text object to form a reference in GameController object
    8. Update the DestroyByContact with:
      if (other.tag == "Player"){
      		Instantiate(playerExplosion, other.transform.position, other.transform.rotation);
      		gameController.GameOver();
      	}
  24. Deploy the game
    1. File -> Build settings
    2. Web Player
    3. Build
Written by Nikola Brežnjak