This document is about: QUANTUM 2
SWITCH TO

Bomber

Level 4

Overview

The Quantum Bomber sample is provided with full source code and demonstrates how to build bomberman-like gameplay in Quantum.

Download

Version Release Date Download
2.1.6 Jul 18, 2023 Quantum Bomber 2.1.6 Build 268

Before You Start

To run the sample in online multiplayer mode, first create a Quantum AppId in the PhotonEngine Dashboard and paste it into the AppId field in PhotonServerSettings asset. Then load the Menu scene in the Scenes menu and press Play.

Technical Info

  • Unity: 2021.3.13f1 or higher
  • Platforms: PC (Windows)

Highlights

Technical

  • Custom Movement component and system tailored to top-down grid based games
  • Semi-procedural generation of map and power-up spawning
  • Character customization
  • Time-based explosion spread
  • Zero-event simulation approach
  • Relatively puristic ECS simulation architecture

Gameplay

  • Battle royale bomber
  • Place bomb
  • Power-ups in the form of modifiers for bomb amount, explosion reach and movement speed.

Controls

  • WASD for Movement
  • Space to place bombs

Cell

The Cell is a DSL defined struct which holds a single value in the form of the CellType flag. To access and modify these values more conveniently, the struct has been extended with properties and methods in the Cell.User.cs script.

Each cell is aware of what is the type of object currently at its position, but it does not have any reference to the entities themselves. When a system iterates over an Entity, it checks whether there is anything of interest in the cell. For instance, if the cell IsBurning, then it will trigger the appropriate response on the component of which it is in charge.

Since CellType is a flag, all properties can be constructed as bitwise operations which makes them very efficient.

Grid

The Grid is a regular CSharp class containing the GridSettings struct, an array of Cell structs and a pointer.

Frame.User

By having the Grid defined as a regular class, and having an instance thereof in Frame.User it is possible to both make it available through the regular Frame.XYZ Api while encapsulating all the relevant methods in Frame.Grid.XYZ.

The downside of the Grid being a regular class is that its data resides outside of the regular DSL generated memory. It is therefore necessary to manually handle the initialization, serialization, copying, clearing and dumping of the data. This is done by hooking into the following methods inside of Frame.User:

  • InitUser(): Called once when the frame is initially created.
  • FreeUser(): Called when the Frame is destroyed.
  • CopyUser(): Called when the previous frame state is copied into the next frame.
  • SerializeUser(): Called when the frame is serialized (e.g. as a buddy snapshot for late joiners).
  • DumpFrameUser(): Called when a desync happens.

Grid Settings

The information on how to generate and setup the grid is found in the GridSettings struct. Using the information held in there, it is possible to setup the 1D-array at runtime and populate it with the locations of fixed and destructible blocks, as well as infer the positions of the SpawnPoints.

Cell Array aka Grid

The grid cells are contained in a 1D-array of Cells. The principle is the same as the one as for the Tilemap Asset in the Tilemap Pathfinder Tech Sample. 1D-arrays are supported by the DSL; however, the DSL requires the size of the array to be known at compile time, since the grid is generated at runtime based on the size requested by the players it has to be handled differently.

The array is created at runtime by hooking into the InitUser() method in Frame.User.

Since this is a regular CSharp context, the memory associated with the array will be collected by the garbage collector when the class is destroyed.

The 1D-array inside of the Grid is made compatible with predict-rollback and late-join serialization by hooking into the CopyUser() and SerializeUser() methods inside of Frame.User.

Performance

Despite a Cell only containing a byte-flag, the amount of read and writes done by copying the frame can create significant performance bottleneck. To prevent this, several get-set methods for the grid and its cells have been defined in Grid.cs. These methods user pointer math to return pointers to cells, thus allows to both read and write directly to the struct.

Pointer math is fast. However, a mistake in calculating the offset will result in reading random memory and will result in undefined at best or crash the whole simulation at worst. Use with care!

Broadphase Set and Clear

There are two systems which write memory to the grid:

  • SetBroadphaseSystem: runs before all gameplay systems and sets the cell type based on the entities it currently contains.
  • ClearBroadphaseSystem: runs after all gameplay systems and clears the information in the cells for entities about to be destroyed.

Input

The input struct is made of 5 buttons.

C#

input {
    Button MoveUp;
    Button MoveDown;
    Button MoveLeft;
    Button MoveRight;
    Button PlaceBomb;
}

This is the most condense form the input struct can take. Although the Button type take ups 1-byte in the simulation, it is compressed to 1-bit over the wire.

To prepare the player input to be consumed by the gameplay systems, the InputSystem constructs a Direction using the movement buttons and sets the WantsToPlaceBomb boolean in the AbilityPlaceBomb based on the PlaceBomb button value.

Bomber

The Bomber component is a flag-component to identify and filter entities.

As such the associated BomberSystem is relatively simple since its only job is to check whether a bomber type entity is standing on a burning cell during a given Update().

C#

public override void Update(Frame f, ref BomberFilter filter)
{
    var gridPosition = filter.Transform->Position.RoundToInt(Axis.Both);
    var isInvincible = false;
#if DEBUG
    // Used for debugging purposes
    isInvincible = f.RuntimeConfig.IsInvincible;    
#endif
    if (isInvincible == false && f.GetCellRef(gridPosition).IsBurning)
    {
        // Death animation is triggered from OnEntityDestroyed
        f.Destroy(filter.Entity);
    }
}

Bombs

Bombs are temporary entities which are created by a player action and results in an explosion when their Timer expires.

There are two systems which interact with the Bomb component:

  • AbilityPlaceBombSystem: checks whether a player has pressed the PlaceBomb button and whether the conditions are met to place one.
  • BombSystem: starts the Timer on the bomb, handles chain reactions and triggers an explosion when a bomb is destroyed (either because the timer expired or another bomb's explosion touched it).

Explosion

Explosions last a certain amount of time. Therefore they need to exist as entities rather than just being a raycast. The ExplosionSystem handles the explosion lifetime as well as the explosion spread from cell to cell.

An explosion entity is made of two components:

  • Explosion: a component used as both a flag-component as well as to hold the information about the explosion config.
  • Timer: the timer is used to calculate the remaining time until the explosion spreads to the next adjacent cells.

Power-Ups

The power-up functionalities are provided through two main components

  • PowerUp: defines a power-up by type and modifier amount; and,
  • PowerUpManager: holds the list of spawnable power-ups and the spawn probability.

Power-ups have a chance of spawning when a destroyable block is cleared from the grid. Upon the destruction of a block, the PowerUpManager is informed of the newly cleared position. The PowerUpManagerSystem iterates through the newly available locations and attemps to spawn a random power-up if the cell is empty and not currently on fire.

Movement

The movement is unrestricted inside the cells themselves and reads the grid to determine where the character is allowed to go.

Direction

Direction is a byte-flag which contains the currently pressed movement buttons.

Movement Component

The Movement component keeps track of the relevant movement values between frames.

  • FP CurrentSpeed: the current speed at which the character is moving.
  • FP MaxSpeed: the maximum speed at which the character is allowed to move.
  • Boolean IsMoving: keeps track of whether the last movement input resulted in a move.
  • Boolean LastMoveWasHorizontal: keeps track of the last move's orientation.
  • Direction LastNewInput: keeps track of the latest input provided by the player in both vertical and horizontal directions.
  • Direction CurrentInput: the currently pressed movement directions.
  • FPVector2 MoveDirection: the last movement direction
  • FP StartRotation: the last look rotation prior to a direction change.
  • FP TargetRotation: the look rotation the character should have after a direction change.
  • int RotationStartTick: the tick at which a rotation towards a new direction has started.
  • FP RotationDuration: the maximum total duration a rotation should take.
  • FP RotationTimeMultiplier: the rotation speed multiplier required to achieve a full rotation within the allotted time.

Movement System

The movement system fulfills three core functionalities:

  1. Calculate the possible movement based on the currently pressed movement keys (GetMovementResult()) and return a MoveResult struct containing the values.
  2. Update the current movement speed (UpdateCurrentSpeed())
  3. Move the character (UpdateMovement())
  4. Rotate the character (UpdateRotation())

The MovementSystem implements two features to offer a smooth continuous diagonal movement across the grid:

  • CanMoveInDirection() accounts for corner slide. This enables a character to smoothly move around a corner when a diagonal direction is being pressed.
  • GetMoveVector() contains a direction toggle to alternate which direction (vertical or horizontal) to check first. This allows the character to continuously move diagonally through the grid.

MoveResult

MoveResult is a utility struct. It holds the result of processing the current Movement.CurrentInput against the grid cells on which the character would be moving. Since these values are not used as part of the frame state, it is defined as a regular CSharp struct rather than in the DSL.

  • FPVector2 Direction: the desired movement direction.
  • FP MaxDistance: the maximum possible distance the character can move in the desired direction.
  • FPVector2 LookDirection: the direction in which the character will be looking as a result of this movement.
  • FP RotationTimeMultiplier: the speed at which the character should switch their look direction based on the angle between its current look direction and desired look direction to complete the rotation in the pre-defined amount of time.

3rd Party Assets

The Bomber Sample includes several assets provided courtesy of their respective creators. The full packages can be acquired for your own projects at their respective site:

IMPORTANT: To use them in a commercial project, it is required to purchase a license from the respective creators.

Back to top