6 - Asteroids
Overview
With the ship player entity ready the next step is to spawn in asteroids.
Asteroid Spawner Config
Asteroids are spawned in waves each wave containing more asteroids. An asset object used as a config file is a better way to store the parameters that declare how asteroids are spawned instead of hard coding the variables related to asteroid spawning into a system. Create a new AsteroidGameConfig
script in the QuantumUser > Simulation
folder. Add the following code to it:
C#
using Photon.Deterministic;
using UnityEngine;
namespace Quantum.Asteroids
{
public class AsteroidsGameConfig: AssetObject
{
[Header("Asteroids configuration")]
[Tooltip("Prototype reference to spawn asteroids")]
public AssetRef<EntityPrototype> AsteroidPrototype;
[Tooltip("Speed applied to the asteroid when spawned")]
public FP AsteroidInitialSpeed = 8;
[Tooltip("Minimum torque applied to the asteroid when spawned")]
public FP AsteroidInitialTorqueMin = 7;
[Tooltip("Maximum torque applied to the asteroid when spawned")]
public FP AsteroidInitialTorqueMax = 20;
[Tooltip("Distance to the center of the map. This value is the radius in a random circular location where the asteroid is spawned")]
public FP AsteroidSpawnDistanceToCenter = 20;
[Tooltip("Amount of asteroids spawned in level 1. In each level, the number os asteroids spawned is increased by one")]
public int InitialAsteroidsCount = 5;
}
}
Note that the class inherits from AssetObject
this turns it into a ScriptableObject
that can be injected into the Quantum simulation just like the MapData
and SystemConfig
assets earlier in the tutorial.
AssetRef<EntityPrototype>
is a special type. AssetRef is Quantum's way of referencing AssetObjects
in the asset database. The other fields are simple config values that will be used by the system responsible for spawning asteroids.
To create an instance of the asset right-click on the Resources
folder and select Create > Quantum > Asset.. > AsteroidsGameConfig
. Name the new config asset AsteroidsGameConfig
. Keep all the values as the default values and drop the AsteroidLargeEntityPrototype
into the AsteroidPrototype
field of the config.
Injecting the Config
There are two common ways to get access to a AssetObject
inside the Quantum simulation
- Have a
AssetRef
field on an entity component on a scene object or prefab. - Link the
AssetObject
as a global object viaRuntimeConfig
.
In this case the later approach is used since the game config is a global config that can be used by many systems.
Create a new script in the QuantumUser > Simulation
folder and name it RuntimeConfig.Asteroids
. Add the following code to it:
C#
namespace Quantum
{
public partial class RuntimeConfig
{
public AssetRef<Asteroids.AsteroidsGameConfig> GameConfig;
}
}
Note that the Quantum
instead of the Quantum.Asteroids
namespace has to be used because this class is a partial class extending an existing RuntimeConfig
class.
Return to Unity and head to the QuantumDebugRunner
GameObject in the scene. There is a new GameConfig
field on the RuntimeConfig
entry now. Drop in the AsteroidGameConfig
asset created earlier.
Asteroid Spawner System
Create a new c# script in the QuantumUser > Simulation
folder and name it AsteroidsWaveSpawnerSystem
. Add the following code to it:
C#
using Photon.Deterministic;
using UnityEngine.Scripting;
namespace Quantum.Asteroids
{
[Preserve]
public unsafe class AsteroidsWaveSpawnerSystem : SystemSignalsOnly
{
public void SpawnAsteroid(Frame f, AssetRef<EntityPrototype> childPrototype)
{
AsteroidsGameConfig config = f.FindAsset(f.RuntimeConfig.GameConfig);
EntityRef asteroid = f.Create(childPrototype);
Transform2D* asteroidTransform = f.Unsafe.GetPointer<Transform2D>(asteroid);
asteroidTransform->Position = GetRandomEdgePointOnCircle(f, config.AsteroidSpawnDistanceToCenter);
asteroidTransform->Rotation = GetRandomRotation(f);
if (f.Unsafe.TryGetPointer<PhysicsBody2D>(asteroid, out var body))
{
body->Velocity = asteroidTransform->Up * config.AsteroidInitialSpeed;
body->AddTorque(f.RNG->Next(config.AsteroidInitialTorqueMin, config.AsteroidInitialTorqueMax));
}
}
public static FP GetRandomRotation(Frame f)
{
return f.RNG->Next(0, 360);
}
public static FPVector2 GetRandomEdgePointOnCircle(Frame f, FP radius)
{
return FPVector2.Rotate(FPVector2.Up * radius , f.RNG->Next() * FP.PiTimes2);
}
}
}
This system currently only contains a function that can be called to spawn an asteroid. The function gets the prototype of the asteroid entity to spawn by finding the GameConfig
asset that was linked to the RuntimeConfig
earlier.
FindAsset
is very efficient and there is no problem with calling it repeatedly whenever an asteroid is spawned.
Besides creating the asteroid entity the function initializes the asteroid with a random position and rotation and applies velocity and torque to it based on the values in the GameConfig
.
Adding State to a System
With the SpawnAsteroid
function ready it is time to implement waves. Asteroids should spawn in waves and each wave should spawn one more asteroid than the previous wave. For that it is necessary to keep track of a wave counter.
Systems in ECS are stateless so adding a regular variable to the AsteroidsWaveSpawnerSystem
is not allowed. This is not only done as a good practice in Quantum, it is necessary for the predict-rollback simulation to run correctly. Never put state on a system in Quantum.
Instead of putting state on a system the following two approaches can be used.
- Put state on the global frame. The global frame contains singleton variables that are accessible to all systems.
- Put state on a singleton component. Singleton components are like regular entity components but in addition they can be easily fetched via convenient API. You can learn more about singleton components here.
For simplicity in this tutorial the global frame is used. Create a new Global.qtn
file in the QuantumUser > Simulation
folder. Add the following code to it:
global
{
Int32 AsteroidsWaveCount;
}
This adds the int to the global frame which makes it accessible from any system by calling frame.Global->AsteroidsWaveCount
.
Adding Waves
With the wave counter implemented return to the AsteroidWaveSpawnerSystem
and add the following function:
C#
private void SpawnAsteroidWave(Frame f)
{
AsteroidsGameConfig config = f.FindAsset(f.RuntimeConfig.GameConfig);
for (int i = 0; i < f.Global->AsteroidsWaveCount + config.InitialAsteroidsCount; i++)
{
SpawnAsteroid(f, config.AsteroidPrototype);
}
f.Global->AsteroidsWaveCount++;
}
This function spawns a number of asteroids based on how many waves have spawned and then increments the counter.
Finally, to get the first wave to spawn the OnInit
function can be used which is called on every system once the simulation starts:
C#
public override void OnInit(Frame f)
{
SpawnAsteroidWave(f);
}
With the implementation of the wave spawning system complete return to Unity. There is one last step needed to make the wave system run. Add it to the system list in the AsteroidSystemConfig
.
Identifying the Ship
When entering play mode now there are many null reference errors in the console from the AsteroidsShipSystem
even though that system has not been changed at all. The reason for that is the systems filter which is used:
C#
public struct Filter
{
public EntityRef Entity;
public Transform2D* Transform;
public PhysicsBody2D* Body;
}
Since both the ship and the asteroids have a transform and a physics body the filter finds both objects and runs the ship update function over every asteroid. A simple way to fix this is to add a unique component to the ship.
Create a new AsteroidsShip
.qtn file in the QuantumUser > Simulation
folder. Add an empty component to it like this:
C#
component AsteroidsShip
{
}
NOTE: Empty components in ECS are referred to as tag components. As their use case is to identify entities just like with a tag system.
Return to Unity and open the AsteroidsShip
Prefab. On the QuantumEntityPrototype
component press on the +
button on the Entity Components
list and select the AsteroidsShip
component.
Finally, adjust the filter in the AsteroidsShipSystem
to include the component:
C#
public struct Filter
{
public EntityRef Entity;
public Transform2D* Transform;
public PhysicsBody2D* Body;
public AsteroidsShip* AsteroidsShip;
}
Return to Unity and enter play mode. 5 asteroids are spawned in addition to the player object as part of the first wave.
Back to top