Space Caravan - playable game (my version of Laser Defender)

Here is the URL: https://simmer.io/@DefoltUser/space-caravan

Main menu:

Gameplay:

Game Over screen:

Features what I have designed and implemented on my own (and with help of Google of course):

  • Enemies can rotate towards the path

  • Loot drop system

  • Player can upgrade its main weapon damage per shot

  • Missile launch on R button and splash damage

  • Enemy knows how to aim at the player

  • Autofire for player

  • Interesting wave design - the player needs a little bit of tactical approach to play comfortably

What have I done with the Enemy?

So, as you can see, I did various upgrades.

First of all Enemy Pathing script was upgraded.

New variables:

    [SerializeField] float rotSpeed = 360f;
    [SerializeField] bool rotateTowardsPath = true;

Upgraded move method:

    private void Move()
    {
        if (waypointIndex <= waypoints.Count - 1)
        {
            nextWaypoint = waypoints[waypointIndex].transform.position;
            var movementThisFrame = waveConfig.GetMoveSpeed() * Time.deltaTime;
            transform.position = Vector2.MoveTowards
                                (transform.position, nextWaypoint, movementThisFrame);

            if (rotateTowardsPath)
            {
                RotationTowardsPathMethod();
            }

            if (transform.position == nextWaypoint)
            {
                waypointIndex++;
            }
        }
        else
        {
            Destroy(gameObject);
        }
    }

Tough RotationTowardsPath method:

    private void RotationTowardsPathMethod()
    {
        Vector3 dir = waypoints[waypointIndex].transform.position - transform.position;
        dir.Normalize();
        float zAngle = Mathf.Atan2(dir.y, dir.x) * Mathf.Rad2Deg - 90;
        Quaternion desiredRot = Quaternion.Euler(0, 0, zAngle);
        transform.rotation = Quaternion.RotateTowards(transform.rotation, desiredRot, rotSpeed * Time.deltaTime);
    }

It was not easy to make Rotation work properly, so I explored YouTube, Google and finally found a solution. If you want - take it to your projects. Here some informative links:

Enemy aiming system variables:

    [Header("Aiming on the player")]
    [SerializeField] bool isSearchingForPlayer = false;
    Transform playerCoordinates; 
    [SerializeField] float rotationSpeed = 360f;  
    GameObject playerCaptured; 

At Start Enemy searches for player:

    void Start()
    {
        shotCounter = Random.Range(minTimeBetweenShots, maxTimeBetweenShots);
        playerCaptured = GameObject.Find("Player");
        lootManager = GetComponent<LootManager>();
    }

Update method in the Enemy.cs:

    void Update()
    {
        if(isSearchingForPlayer)
        {
            SearchForPlayer();
            TurnTowardsPlayer();
        }

        CountDownAndShoot();
        
    }

Two methods which make the aiming possible:

    private void TurnTowardsPlayer()
    {
        Vector3 dir = playerCoordinates.position - transform.position; 
        dir.Normalize(); 
        float zAngle = Mathf.Atan2(dir.y, dir.x) * Mathf.Rad2Deg - 90; 

        Quaternion desiredRotation = Quaternion.Euler(0, 0, zAngle); 
        transform.rotation = Quaternion.RotateTowards(transform.rotation, desiredRotation, rotationSpeed * Time.deltaTime); 

    }

    private void SearchForPlayer()
    {
        if (playerCaptured == null)
        {
            playerCaptured = GameObject.Find("Player");
        }
        else
        {
            playerCoordinates = playerCaptured.transform;
        }
    }

LootManager script connected in Enemy.cs:

    LootManager lootManager;


    // Start is called before the first frame update
    void Start()
    {
        shotCounter = Random.Range(minTimeBetweenShots, maxTimeBetweenShots);
        playerCaptured = GameObject.Find("Player");
        lootManager = GetComponent<LootManager>();
    }

And called when the enemy dies:

    private void Die()
    {
        FindObjectOfType<GameSession>().AddToScore(scoreValue);

        lootManager.LootSpawn();
        //Debug.Log("After LootSpawn line activated");

        Destroy(gameObject);
        GameObject explosion = Instantiate(deathVFX, transform.position, transform.rotation);
        Destroy(explosion, durationOfExplosion);
        AudioSource.PlayClipAtPoint(deathSound, Camera.main.transform.position, deathSoundVolume);
    }

The LootManager.cs itself:

public class LootManager : MonoBehaviour
{
    [SerializeField] GameObject lootPrefab;
    [SerializeField] float minProbability = 0.3f;
    [SerializeField] AudioClip spawnSound;
    [SerializeField] [Range(0, 1)] float spawnSoundVolume = 0.7f;
    float spawnProbability;


    public void LootSpawn()
    {
        spawnProbability = Random.Range(0f, 1f);
        //Debug.Log("Spawn Probability is: ");
        //Debug.Log(spawnProbability);

        if (spawnProbability > minProbability)
        {
            GameObject loot = Instantiate(lootPrefab, transform.position, transform.rotation);
            AudioSource.PlayClipAtPoint(spawnSound, Camera.main.transform.position, spawnSoundVolume);
        }
        else
        {
            return;
        }

    }
}

What is the idea of LootBeahaviour.cs ?

The idea is to choose the type of loot. It can be additional damage or health repair or score.
The script must be added to the loot prefabs. How it looks:

public class LootBehavior : MonoBehaviour
{
    [Header("Loot Movement")]
    [SerializeField] float lootVelocity = 1f;

    [Header("Attack")]
    [SerializeField] bool attackAmpl = false;
    [SerializeField] int attackAdded = 5;
    [SerializeField] AudioClip attackBoostSound;
    [SerializeField] [Range(0, 1)] float attackBoostVolume = 0.7f;

    [Header("Repair")]
    [SerializeField] bool repairShip = false;
    [SerializeField] int healthRepaired = 100;
    [SerializeField] AudioClip repairBoostSound;
    [SerializeField] [Range(0, 1)] float repairBoostVolume = 0.7f;

    [Header("Score")]
    [SerializeField] bool ptsLoot = false;
    [SerializeField] int scoreGained = 250;
    [SerializeField] AudioClip scoreBoostSound;
    [SerializeField] [Range(0, 1)] float scoreBoostVolume = 0.7f;

    Player player; // через это вызываем метод в игроке
    GameSession gameSession;


    // Start is called before the first frame update
    void Start()
    {
        player = GameObject.Find("Player").GetComponent<Player>();
        gameSession = FindObjectOfType<GameSession>();
    }

    // Update is called once per frame
    void Update()
    {
        Vector2 lootPosition = transform.position;
        Vector2 lootMovement = new Vector2(0, lootVelocity * Time.deltaTime);
        lootPosition -= lootMovement;
        transform.position = lootPosition;
    }

    private void OnTriggerEnter2D(Collider2D other)
    {
        if (other.tag == "Player")
        {
            PowerUpOptions();
        }
        else if (other.tag == "GameBoundary")
        {
            Destroy(gameObject);
        }
        else
        {
            Debug.Log("3rd condition destruction");
            Destroy(gameObject);
        }
    }

    private void PowerUpOptions()
    {
        if (attackAmpl)
        {
            //Debug.Log("Attack boost");
            AudioSource.PlayClipAtPoint(attackBoostSound, Camera.main.transform.position, attackBoostVolume);
            Destroy(gameObject);
            player.AddWeaponDamage(attackAdded);
        }
        else if (repairShip)
        {
            //Debug.Log("Repair boost");
            AudioSource.PlayClipAtPoint(repairBoostSound, Camera.main.transform.position, repairBoostVolume);
            Destroy(gameObject);
            player.AddHealth(healthRepaired);
        }
        else if (ptsLoot)
        {
            //Debug.Log("Pts boost");
            AudioSource.PlayClipAtPoint(scoreBoostSound, Camera.main.transform.position, scoreBoostVolume);
            Destroy(gameObject);
            gameSession.AddToScore(scoreGained);
        }
        else
        {
            Destroy(gameObject);
        }
    }
}

And how it looks in Inspector:

Pretty easy, isn’t it?

How the player interacts with upgrades?

Watch one more time at a previous script ( LootBehavior : MonoBehaviour) all answers there.

And the code at the Player.cs have some additional features which can connect Player with it’s Lasers prefab DamageDealer Component:

    [Header("Projectile")]
    public GameObject lazerPrefab;
    [SerializeField] int startLazerDamage = 20; 
    [SerializeField] float projectileSpeed = 10f;
    [SerializeField] float projectileFiringPeriod = 0.5f; 
DamageDealer myLaser; 

    // Start is called before the first frame update
    void Start()
    {
        SetUpMoveBoundaries();
        myLaser = lazerPrefab.GetComponent<DamageDealer>();
        myLaser.SetDefaultDamage(startLazerDamage); 
        firingCoroutine = StartCoroutine(FireContinuosly());
    }

And methods in Player.cs:

    public int GetShootDamage()
    {
        int damage = myLaser.GetDamage();
        return damage;
    }

    //public void SetDefaultDamage(int )

     public void AddWeaponDamage(int attackAdded)
     {
        //Debug.Log("Attack added: ");
        //Debug.Log(attackAdded);
        this.attackAdded = attackAdded;
        myLaser.AddWeaponDamage(attackAdded);
           //lazerDamage = myLaser.SetDamage(attackAdded);
     } 

    public void AddHealth(int healthRepaired)
    {
        //Debug.Log("Health added: ");
        //Debug.Log(healthRepaired);
        health += healthRepaired;
    }

DamageDealer.cs have some additional methods too:

    public void AddWeaponDamage(int attackAdded)
    {
        //Debug.Log("Attack added TO PLAYER'S LAZER: ");
        //Debug.Log(attackAdded);
        damage += attackAdded;
        
           //lazerDamage = myLaser.SetDamage(attackAdded);
     }

    public void SetDefaultDamage(int defaultDamage)
    {
        damage = defaultDamage;
    }

Player Rocket launch and splash damage system
It uses Missile.cs script. The script is the same for enemy missiles and player missile:

public class Missile : MonoBehaviour
{
    [SerializeField] float misileSpeed = 4f;

    [SerializeField] GameObject misileExplosion;
    [SerializeField] AudioClip explosionSound;
    [SerializeField] [Range(0, 1)] float soundVolume = 0.3f;

    [Header("Splash settings")]
    [SerializeField] bool splashMode = false;
    [SerializeField] float splashRange = 2.55f;



    // Update is called once per frame
    void Update()
    {
        Vector3 missilePos = transform.position;

        Vector3 missileVelocity = new Vector3(0, misileSpeed * Time.deltaTime, 0);

        missilePos += transform.rotation * missileVelocity;
        transform.position = missilePos;

    }

    private void OnTriggerEnter2D(Collider2D other)
    {
        if (other.tag == "Player" || other.tag == "Enemy")
        {
            MissileBehaviour();
        }
        else if (other.tag == "GameBoundary")
        {
            Destroy(gameObject);
        }
    }

    private void MissileBehaviour()
    {
        if (splashMode)
        {
            var splashDamage = gameObject.GetComponent<DamageDealer>().GetDamage();
            var hitColliders = Physics2D.OverlapCircleAll(transform.position, splashRange);

            foreach (var hitCollider in hitColliders)
            {
                var enemy = hitCollider.GetComponent<Enemy>();
                if (enemy)
                {
                    enemy.HitFromSplash(splashDamage);
                }
                Instantiate(misileExplosion, transform.position, Quaternion.identity);
                AudioSource.PlayClipAtPoint(explosionSound, Camera.main.transform.position, soundVolume);
            }
        }
        else
        {
            Destroy(gameObject);
            Instantiate(misileExplosion, transform.position, Quaternion.identity);
            AudioSource.PlayClipAtPoint(explosionSound, Camera.main.transform.position, soundVolume);
        }
    }
}

And how it looks in the Inspector:

Splash Damage tutorial:

There are to much text for one post, I am sorry for overwhelming you with all of this information. I hope these posts could be useful! Thank you for reading!

1 Like

First of all, thank you for sharing all the steps you took to make the changes to the original game!
I am not a fan of shooter game, but I did enjoy playing your game because of the features you put into them, especially because the enemy can aim at the player. It is hard to escape those bullets and you need to come up with a strategy indeed. This makes an highly enjoyable game play!
I also liked your design of the game, not too dark or too bright. The start screen has quite some text to it, but I like it this way. The only thing I would change here is the color when you hover over the buttons, they get too dark.
You did an amazing job! Congrats on it!

1 Like

What a pleasure to read such a detailed review! I glad what the game is playable because the 1st build was absolute hardcore - just about 15 people had troubles with getting more than 10 000 points.

This build is second. The difficulty is much lower than before.

Yes, I agree with you at this point. UI colour makes it hard to percept.

Thank you so much for the time spent at playing my game and writing review, Patricia!

1 Like

You are welcome:)

1 Like

Congatulations on the overall work and effect! Lot of nice features added.
As of my feedback:

  • Sometimes when the large enemy is destroyed and crate is spawned too close to the edge it’s impossible to pick up.
  • Difficulty balance - at first the game is quite challenging but after you upgrade your laser sufficiently it becomes too easy and almost impossible to lose.

The first is easy to fix e.g. with player padding but the second would require some more work and I assume that this is just a small project to learn how to code and it may be not worth investing time in fine-tuning difficulty.

Anyway, congratulations once again, I really enjoyed playing!

1 Like

Hi @tobson! I am glad that you enjoyed my game!

I absolutely agree with your feedback about the cons of “Space Caravan” game.
About difficulty: the waves are looped, of course after two-three cycles game becomes boring. It can become better with new enemies or new playable levels.

For now, this project is done, the only last change that I have done - is mentioning this amazing course in assets list at Game Over screen:

I would like to make scroll shooter/bullet hell game in future, but not now. It needs to be learned:

  • Metagame - upgrade player ship for coins or resources

  • Leaderboards connectable to the Internet (in Google Play service, for example)

  • Achievements (via Steam or Google Play)

  • Advertisement implementation

  • Metagame - upgrade player ship for coins or resources

  • Leaderboards connectable to the Internet (in Google Play service, for example)

  • Achievements (via Steam or Google Play)

  • Advertisement implementation

  • Very interesting and perfectly polished levels

  • And many other very important features

Privacy & Terms