This document is about: QUANTUM 3
SWITCH TO

9 - Shooting

ShipConfig

Currently, values like the acceleration and turn speed of the ship are hard coded into the AsteroidsShipSystem. A much better pattern is to put these variables plus additional variables that will be used to control the ship's shooting into a config file.

Start by creating a new c# script and name it AsteroidsShipConfig. Then add the following code:

C#

using Photon.Deterministic;
using UnityEngine;

namespace Quantum
{
  public class AsteroidsShipConfig : AssetObject
  {
    [Tooltip("The speed that the ship turns with added as torque")]
    public FP ShipTurnSpeed = 8;

    [Tooltip("The speed that the ship accelerates using add force")]
    public FP ShipAceleration = 6;

    [Tooltip("Time interval between ship shots")]
    public FP FireInterval = FP._0_10;
    
    [Tooltip("Displacement of the projectile spawn position related to the ship position")]
    public FP ShotOffset = 1;
    
    [Tooltip("Prototype reference to spawn ship projectiles")]
    public AssetRef<EntityPrototype> ProjectilePrototype;
  }
}

Then add a reference to the ship config in the AsteroidShip.qtn:

C#

component AsteroidsShip
{
    AssetRef<AsteroidsShipConfig> ShipConfig;
}

Create a new instance of the ship config in the Resources folder. Name it DefaultShipConfig. Then link it to the ship prototype by dropping it into the ShipConfig field on the AsteroidsShip GameObject.

Next adjust the code in the UpdateShipMovement function of the AsteroidsShipSystem to use the values from the config:

C#

var config = f.FindAsset(filter.AsteroidsShip->ShipConfig);
FP shipAcceleration = config.ShipAceleration;
FP turnSpeed = config.ShipTurnSpeed;

shooting

First, add a fire interval field to the AsteroidShip.qtn to keep track of the time between firing intervals when shooting.

C#

component AsteroidsShip
{
    AssetRef<AsteroidsShipConfig> ShipConfig;
    FP FireInterval;
}

Next, open the AsteroidsShipSystem and add a function to handle firing:

C#

private void UpdateShipFire(Frame f, ref Filter filter, Input* input)
{ 
    var config = f.FindAsset(filter.AsteroidsShip->ShipConfig);

    if (input->Fire && filter.AsteroidsShip->FireInterval <= 0)
    {
        filter.AsteroidsShip->FireInterval = config.FireInterval;
        // TODO create projectile
    }
    else
    {
        filter.AsteroidsShip->FireInterval -= f.DeltaTime;
    }
}

This function allows the ship to fire at regular intervals when the fire button is pressed. The TODO line will be replaced with code that creates a bullet entity.

Call this function from the Update method of the system after calling UpdateShipMovement:

C#

UpdateShipFire(f, ref filter, input);

Create a new c# script and name it AsteroidsProjectileSystem. Add the following code to it:

C#

namespace Quantum.Asteroids
{
    using Photon.Deterministic;

    public unsafe class AsteroidsProjectileSystem : SystemSignalsOnly
    {
        public void AsteroidsShipShoot(Frame f, EntityRef owner, FPVector2 spawnPosition, AssetRef<EntityPrototype> projectilePrototype)
        {
            EntityRef projectileEntity = f.Create(projectilePrototype);
            Transform2D* projectileTransform = f.Unsafe.GetPointer<Transform2D>(projectileEntity);
            Transform2D* ownerTransform = f.Unsafe.GetPointer<Transform2D>(owner);


            projectileTransform->Rotation = ownerTransform->Rotation;
            projectileTransform->Position = spawnPosition;
        }
    }
}

Add this system to the AsteroidsSystemConfig in Unity.

Next, create a AsteroidsProjectileConfig c# script to act as a config file for projectiles:

C#

using Photon.Deterministic;
using UnityEngine;

namespace Quantum
{
    public class AsteroidsProjectileConfig : AssetObject
    {
        [Tooltip("Speed applied to the projectile when spawned")]
        public FP ProjectileInitialSpeed = 15;

        [Tooltip("Time until destroy the projectile")]
        public FP ProjectileTTL = 1;
    }
}

Finally, create a AsteroidsProjectile.qtn file to act as a data component for the projectile:

C#

component AsteroidsProjectile
{
    FP TTL;
    EntityRef Owner;
    AssetRef<AsteroidsProjectileConfig> ProjectileConfig;
}

Adjust the AsteroidsShipShoot function in the AsteroidsProjectileSystem by adding the following line to initialize the projectile to the end of it:

C#

AsteroidsProjectile* projectile = f.Unsafe.GetPointer<AsteroidsProjectile>(projectileEntity);
var config = f.FindAsset(projectile->ProjectileConfig);
projectile->TTL = config.ProjectileTTL;
projectile->Owner = owner;

PhysicsBody2D* body = f.Unsafe.GetPointer<PhysicsBody2D>(projectileEntity);
body->Velocity = ownerTransform->Up * config.ProjectileInitialSpeed;

Signals

AsteroidsShipShoot should be called whenever the ship is shooting. However, the function is in a different system. One solution to call it would be to make the function static and call it directly however this lead to tight coupling of systems which is not ideal especially for larger code bases.

A solution for that is to use signals. Signals are an event based way for systems to communicate to each other. Signals can be defined in any .qtn file. In this case open the AsteroidsProjectile.qtn and add a signal to it like this:

C#

component AsteroidsProjectile
{
    FP TTL;
    EntityRef Owner;
    AssetRef<AsteroidsProjectileConfig> ProjectileConfig;
}

signal AsteroidsShipShoot(EntityRef owner, FPVector2 spawnPosition, AssetRef<EntityPrototype> projectilePrototype);

Now, have the AsteroidsProjectileSystem implement the ISignalAsteroidShipShoot interface.

C#

public unsafe class AsteroidsProjectileSystem : SystemSignalsOnly, ISignalAsteroidsShipShoot

This interface causes the AsteroidShipShoot function to be called whenever the signal is invoked.

To invoke the signal adjust the AsteroidsShipSystem by replacing the // TODO create projectile line with the following:

C#

var relativeOffset = FPVector2.Up * config.ShotOffset;
var spawnPosition = filter.Transform->TransformPoint(relativeOffset);
f.Signals.AsteroidsShipShoot(filter.Entity, spawnPosition, config.ProjectilePrototype);

Creating the Projectile Entity

Create a new Quantum > 2D > Circle Entity GameObject in the scene. Name it AsteroidsProjectile. Remove the MeshRenderer and MeshFilter from it. Adjust the radius of the circle collider to 0.16 and check the PhysicsBody2D checkbox.

Add a AsteroidsProjectile component in the Entity Components list of the QuantumEntityPrototype. The TTL and Owner fields are assigned at runtime via the config, so they can be kept empty. For the config field create a new AsteroidsProjectileConfig asset and name it DefaultProjectileConfig. Set the Projectile Initial Speed to 20 and the Projectile TTL to 2 then assign the config to the projectile in the config field of the component.

Create a 3D > Cube GameObject as the child of the AsteroidsProjectile and scale it down to (0.17, 0.17, 0.17). This will act as the visual of the projectile. Remove the box collider from the object.

Drag the AsteroidsProjectile into the Resources folder to make it a prefab then remove it from the scene. Next link the prototype of the prefab to the Projectile Prototype field of the DefaultShipConfig.

Time to Live

Currently, projectiles do not get destroyed yet. Projectiles can be destroyed in two ways. Either once their time to live runs out or when colliding with an asteroid or spaceship.

To destroy projectiles after their TTL expires a system needs to check them each frame. Have the AsteroidsProjectileSystem inherit from SystemMainThreadFilter<AsteroidsProjectileSystem.Filter> instead of SystemSignalsOnly and add the following to it:

C#

public struct Filter
{
    public EntityRef Entity;
    public AsteroidsProjectile* Projectile;
}

public override void Update(Frame f, ref Filter filter)
{
    filter.Projectile->TTL -= f.DeltaTime;
    if (filter.Projectile->TTL <= 0)
    {
        f.Destroy(filter.Entity);
    }
}

Enter play mode. The spaceship can shoot now by pressing space. The projectiles automatically despawn after 2 seconds, however they currently collide with asteroids and the players as if they were regular physics bodies. In the next part of the tutorial collision detection will be added to the bullets.

Back to top