A decorative background entity that drifts around the playfield. It stores size, color, movement bias and sibling list, accelerates up to a layer-scaled max speed, detects collisions with other background blocks and arena bounds, briefly stops on impact, snaps to pixel grid, and then picks a new biased/random direction.
using System;
using System.Collections.Generic;
namespace BlockParty;
/// <summary>
/// Cosmetic, faceless block that drifts around the backdrop. Port of the original
/// <c>BackgroundBlock</c>: a solid coloured square that accelerates in a random direction,
/// collides with its same-layer siblings and the arena bounds, stops briefly on impact, then
/// picks a new direction. Purely decorative — it never interacts with gameplay entities and
/// only ever tests collision against the blocks in its own depth layer.
/// </summary>
public sealed class BackgroundBlock : Entity2D
{
private const float MIN_SPEED = 0.0f;
private const float BASE_MAX_SPEED = 160.0f;
private const float REQUIRED_COLLISION_SPEED = 40.0f;
private const float REQUIRED_COLLISION_TIME = 0.33f; // collide-time before switching direction
public Direction MoveDirection { get; private set; }
private float _moveSpeed;
private float _maxSpeed;
private float _acceleration;
private float _currentCollidingTime;
private bool _isStopped;
private float _switchTimer; // while stopped, counts down to picking a new direction
private Color _color;
private List<BackgroundBlock> _layerSiblings; // the only blocks we collide against
private BackgroundDriftBias _driftBias;
/// <summary>Configure the block. <paramref name="layerSiblings"/> is the list of all blocks
/// in this block's depth layer (used for collision; the original keyed off the layer index).</summary>
public void Setup( Vector2 size, Color color, int layer, List<BackgroundBlock> layerSiblings, float speedMultiplier, BackgroundDriftBias driftBias )
{
Size = size;
_color = color;
_layerSiblings = layerSiblings;
_driftBias = driftBias;
MoveDirection = GetBiasedDirection();
_moveSpeed = MIN_SPEED;
// Deeper layers accelerate more slowly (a subtle parallax tell).
float speed = Math.Clamp( speedMultiplier, 0f, 3f );
_acceleration = (25f - ((layer + 1) * 5f)) * speed;
_maxSpeed = BASE_MAX_SPEED * speed;
}
public void CreateVisuals( int childOrder = 0 )
{
var sr = SpriteLayer.Add( GameObject, "sprites/pixel.sprite", Size, "idle", childOrder );
sr.Color = _color;
// The level's background-block opacity rides in the tint's alpha, so these must alpha-blend:
// opt out of the opaque default and zero the cutoff (which DISCARDS pixels under 0.5), the same
// pairing every other tint-translucent sprite uses (lane overlays / Portal rings / field motes).
sr.Opaque = false;
sr.AlphaCutoff = 0f;
}
public override void Tick( float dt )
{
if ( _isStopped )
{
_switchTimer -= dt;
if ( _switchTimer <= 0f )
{
MoveDirection = GetNewDirection();
_isStopped = false;
_moveSpeed = MIN_SPEED;
}
return;
}
Velocity = Globals.GetVectorForDirection( MoveDirection ) * _moveSpeed;
Pos += Velocity * dt;
bool collidingForward = IsColliding( X, Y, MoveDirection, out var other );
if ( collidingForward )
{
if ( other != null )
Unpenetrate( X, Y, other );
_currentCollidingTime += dt;
if ( _moveSpeed > REQUIRED_COLLISION_SPEED || _currentCollidingTime >= REQUIRED_COLLISION_TIME )
Impact();
}
else
{
_moveSpeed = Math.Clamp( _moveSpeed + _acceleration * dt, MIN_SPEED, _maxSpeed );
_currentCollidingTime = 0f;
}
ClampToBounds( X, Y );
}
private void Impact()
{
_isStopped = true;
// Snap to the integer pixel grid on rest so accumulated sub-pixel travel drift never lands
// on the cross axis and causes phantom corner collisions when the block next turns 90°.
// Rendering already rounds the position, so this is visually a no-op. (See Block.ImpactEffects.)
Pos = new Vector2( MathF.Round( Pos.x ), MathF.Round( Pos.y ) );
_currentCollidingTime = 0f;
_switchTimer = Rng.BackgroundFloat( 0.33f, 0.70f );
}
// ----------------------------------------------------------------------------------------
private bool IsColliding( float x, float y, Direction dir, out BackgroundBlock other )
{
if ( IsCollidingWithSibling( x, y, out other ) )
return true;
other = null;
return !IsInBounds( x, y, dir );
}
private bool IsColliding( Direction dir )
{
Vector2 offset = Globals.GetVectorForDirection( dir );
return IsColliding( X + offset.x, Y + offset.y, dir, out _ );
}
private bool IsCollidingWithSibling( float x, float y, out BackgroundBlock other )
{
other = null;
foreach ( var b in _layerSiblings )
{
if ( b != this && DoesPenetrate( x, y, b ) )
{
other = b;
return true;
}
}
return false;
}
private Direction GetNewDirection()
{
int tries = 0;
Direction next = MoveDirection;
while ( tries < 10 )
{
tries++;
next = GetBiasedDirection();
if ( next == Globals.GetOppositeDirection( MoveDirection ) && tries < 5 )
continue;
if ( next != MoveDirection && !IsColliding( next ) )
return next;
}
return next;
}
private Direction GetBiasedDirection()
{
if ( _driftBias == BackgroundDriftBias.None )
return RandomBackgroundDirection();
if ( Rng.BackgroundValue() >= 0.8f )
return RandomBackgroundDirection();
if ( _driftBias == BackgroundDriftBias.Horizontal )
return Rng.BackgroundInt( 0, 2 ) == 0 ? Direction.Left : Direction.Right;
if ( _driftBias == BackgroundDriftBias.Vertical )
return Rng.BackgroundInt( 0, 2 ) == 0 ? Direction.Down : Direction.Up;
return _driftBias switch
{
BackgroundDriftBias.Left => Direction.Left,
BackgroundDriftBias.Right => Direction.Right,
BackgroundDriftBias.Down => Direction.Down,
BackgroundDriftBias.Up => Direction.Up,
_ => RandomBackgroundDirection(),
};
}
private static Direction RandomBackgroundDirection()
=> Globals.GetAllDirections()[Rng.BackgroundInt( 0, Globals.GetAllDirections().Length )];
}