01. Architectural Overview

Overview

The Sydewinder game is developed in C#, and some aspects of the game are created using the Sandbox Editor.

Sydewinder uses a single level file named canyon.cry. This level allows viewing all the 3D models and particle effects that are used in the game. Furthermore, it contains markers to help position objects such as the camera and the main menu.

The game is managed by the SydewinderApp class. In every frame, it will which check the global status, spawn new waves, update the positions and clean-up unused elements.

The GamePool class contains references to all the available entities and allows the game loop to clean the entities that are no longer required.

Game Objects Pool

The GamePool maintains all the objects of type DestroyableBase and classes inherited from it. The main purpose is to track newly created objects and clean them later on when they are no longer required. The class simplifies access to the update routines which are provided in the DestroyableBase class.

At the hearth of this class lies the _gameObjects dictionary and several helper collections.

public static class GamePool
{
  private static Dictionary<uint, DestroyableBase> _gameObjects = new Dictionary<uint, DestroyableBase>();
  //...
}

By calling the static AddObjectToPool method and providing a DestroyableBase object as an argument, the object that is being registered will be updated for every cycle.

GamePool.AddObjectToPool(gameObject);

You can remove an object which might be executed in the current cycle by deferring it. For the object to be deferred, you need to provide the Entity ID of the object to the FlagForPurge method.

GamePool.FlagForPurge(entityId);

OnUpdate

The OnUpdate method is the core method for any game and will be called every frame on entity components to update their status. To make use of the OnUpdate method the class has to inherit from EntityComponent. Once a class inherits from EntityComponent they can override the base OnUpdate method to receive the update calls. For Sydewinder the main gameplay loop is handled in the OnUpdate method of the SydewinderApp class, which can be found in the file SydewinderApp.cs.

public class SydewinderApp : EntityComponent
{
  protected override void OnUpdate(float frameTime)
  {
    base.OnUpdate(frameTime);
    Update();
  }
 
  private void Update()
  {
    // Check game status.
    // ...
 
    // Spawn and update environment.
    // ...
 
    // Update player direction and speed.
    // ...
 
    // Update position of game objects including player.
    // ...
 
    // Spawn enemies.
    // ..
  }
}

FrameTime

Every entity gets updated every frame. Because not every device is the same it can happen that the game runs on a different framerate (amount of frames rendered per second) than on the device on which it is developed. To make sure that the gameplay stays almost the same on every device the frameTime can be used. The value of frameTime is the time it took to update the previous frame. The OnUpdate method of the EntityComponent automatically receives the frameTime every time it is called. Another way to get the frameTime is to get the value of FrameTime.Delta. The FrameTime class is a static class that is updated at the start of every frame, which makes it easy to access the frameTime from anywhere in the code.

An common use case for frameTime is moving an entity, which probably should always happen independent of the framerate.

public void Move(Vector3 direction, float speed)
{
  var position = Entity.Position;
  // Normalize the direction before applying the speed to it.
  direction = direction.Normalized * speed;
  // Multiply the direction-vector by the frametime.
  // This makes it independent of the game's framerate.
  position += direction * FrameTime.Delta; 
  Entity.Position = position;
}


Game Status

The static State variable indicates whether the game is currently in play mode or not. This can change when the main menu is opened, or the game is paused. When State is not equal to GameState.Running, the Update method will return immediately so no new objects are created and existing object doesn't move further.

// Check Game Status
// Ensures physics won't kick in while paused.
if (State == GameState.Paused)
{
  GamePool.UpdatePoolForPausedGame();
}
 
if (State != GameState.Running)
{
  return;
}

The UpdatePoolForPauseGame method ensures objects which are influenced by physics stay in the same position by setting the Velocity to Vector3.Zero every frame.

Update Player

The UpdateSpeed method of the Player class handles the user-input every frame. After evaluating the input it will calculate the speed of the player. It also validates the speed so the player can't fly outside the level boundaries.

For more information on key-input evaluation, refer to the Input and Game Controller article.

// Check Key input and update player speed.
if (Playership.Entity.Exists)
{
  Playership.CheckCoolDown();
  Playership.UpdateSpeed();    
}  

Update Other Game Objects

Every frame the UpdatePool method is called. The GamePool iterates over all the registered entities such as the environment-entities (Tunnel and Door classes) and enemy-entities to updates their positions.

// Update position of game objects including player.
GamePool.UpdatePool();

Spawn Enemy

The player flies through an endless tunnel, and large waves of enemies will be spawned to make the game more challenging.

To achieve a precise spawn time of 3 seconds, the FrameTime will be added to the _spawnTimer variable. When the spawn duration is exceeded, the timer resets to 0 and a random wave of enemy ships are positioned outside the screen border to avoid them from appearing immediately on the screen. On the next update cycle the enemy ships will move into the visible portion of the screen.

// Add frame time and check if greater than or equal timer.
_spawnTimer += FrameTime.Current;
if(_spawnTimer >= SPAWN_EVERY_SECONDS)
{        
  // Reset spawn timer.
  _spawnTimer = 0;
    
  var waveType = Random.Next(3);
  var enemyType = Random.Next(3);

  // Increase speed for each difficulty level.
  var waveSpeed = new Vector3(0f, -11f - (HUD.Stage * 2), 0f);
  var spawnPoint = _spawnPoint;

  switch((WaveType)waveType)
  {
  case WaveType.VerticalLine:
    spawnPoint.z = 79;
    Enemy.SpawnWave(spawnPoint, enemyType, WaveType.VerticalLine, waveSpeed);
    break;
  case WaveType.HorizontalLine:
    var waveOffset = Random.Next(3);
    spawnPoint.z = 72 + waveOffset;
    Enemy.SpawnWave(spawnPoint, enemyType, WaveType.HorizontalLine, waveSpeed);
    spawnPoint.z = 64 + waveOffset;
    Enemy.SpawnWave(spawnPoint, enemyType, WaveType.HorizontalLine, waveSpeed);
    break;
  case WaveType.Diagonal:
    spawnPoint.z = 79;
    Enemy.SpawnWave(spawnPoint, enemyType, WaveType.Diagonal, waveSpeed);
    break;
  }
}