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.
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);
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.
// ..
}
}
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;
}
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.
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();
}
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();
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;
}
}