Blitz3D+ Language Reference

Interfaces: Shoot-'Em-Up Enemy Example

Back to Guides

Interfaces and Implements are Extended-mode features. This example demonstrates their value and deliberately limited scope. All examples use modern dialect syntax: a colon annotates a custom Type and a period accesses a field or calls a Method. For exact rules and a runnable program, see the Interfaces language reference and Type Lists language reference.

The problem without interfaces

A shoot-'em-up commonly has drones, turrets, bosses, and other enemies. Each enemy needs to update, receive damage, participate in collision detection, award a score, and clean up its resources. Type methods already let each concrete Type keep its behaviour beside its data:

Type Drone
    Field entity
    Field health
    Field fireCooldown#

    Method Update(world:GameWorld, dt#)
        PointEntity Self.entity, world.playerEntity
        MoveEntity Self.entity, 0, 0, 4.0 * dt

        Self.fireCooldown = Self.fireCooldown - dt
        If Self.fireCooldown <= 0
            FireEnemyBullet Self.entity
            Self.fireCooldown = 1.5
        EndIf
    End Method

    Method ApplyHit(damage)
        Self.health = Self.health - damage
    End Method
End Type

Type Turret
    Field entity
    Field health
    Field rotation#
    Field fireCooldown#

    Method Update(world:GameWorld, dt#)
        Self.rotation = Self.rotation + 30.0 * dt
        RotateEntity Self.entity, 0, Self.rotation, 0

        Self.fireCooldown = Self.fireCooldown - dt
        If Self.fireCooldown <= 0
            FireSpread Self.entity
            Self.fireCooldown = 2.0
        EndIf
    End Method

    Method ApplyHit(damage)
        Self.health = Self.health - damage
    End Method
End Type

The Types are tidy, but the game systems must know every concrete enemy. Updating them requires one loop per Type:

Function UpdateEnemies(world:GameWorld, dt#)
    Local drone:Drone
    For drone = Each Drone
        drone.Update(world, dt)
    Next

    Local turret:Turret
    For turret = Each Turret
        turret.Update(world, dt)
    Next

    Local boss:Boss
    For boss = Each Boss
        boss.Update(world, dt)
    Next
End Function

Damage routing repeats the same knowledge. If collision detection produces an entity handle, the program must search every enemy collection:

Function ApplyEnemyHit(entity, damage)
    Local drone:Drone
    For drone = Each Drone
        If drone.entity = entity
            drone.ApplyHit(damage)
            Return
        EndIf
    Next

    Local turret:Turret
    For turret = Each Turret
        If turret.entity = entity
            turret.ApplyHit(damage)
            Return
        EndIf
    Next

    Local boss:Boss
    For boss = Each Boss
        If boss.entity = entity
            boss.ApplyHit(damage)
            Return
        EndIf
    Next
End Function

Cleanup, scoring, counting, and collision processing acquire similar branches. Adding a Kamikaze Type means revisiting every system. An alternative is one large Enemy Type with a kind field:

Type Enemy
    Field kind
    Field entity
    Field health
    Field velocity#
    Field rotation#
    Field fireCooldown#
    Field phase
    Field phaseTime#
    Field orbitRadius#
    Field targetEntity

    Method Update(world:GameWorld, dt#)
        Select Self.kind
            Case ENEMY_DRONE
                UpdateDrone Self, world, dt
            Case ENEMY_TURRET
                UpdateTurret Self, world, dt
            Case ENEMY_BOSS
                UpdateBoss Self, world, dt
        End Select
    End Method
End Type

This permits one Type list, but every object carries fields it may never use, and every operation grows another Select block.

Defining the common role

An interface describes only what the enemy-management systems require. It contains Method signatures but no fields or implementations:

Interface Enemy
    Method Update(world:GameWorld, dt#)
    Method ApplyHit(damage)
    Method IsDead()
    Method CollisionEntity()
    Method ScoreValue()
    Method OnDestroyed()
End Interface

A concrete Type explicitly promises to provide every signature:

Type Drone Implements Enemy
    Field entity
    Field health
    Field fireCooldown#

    Method Update(world:GameWorld, dt#)
        PointEntity Self.entity, world.playerEntity
        MoveEntity Self.entity, 0, 0, 4.0 * dt

        Self.fireCooldown = Self.fireCooldown - dt
        If Self.fireCooldown <= 0
            FireEnemyBullet Self.entity
            Self.fireCooldown = 1.5
        EndIf
    End Method

    Method ApplyHit(damage)
        Self.health = Self.health - damage
    End Method

    Method IsDead()
        Return Self.health <= 0
    End Method

    Method CollisionEntity()
        Return Self.entity
    End Method

    Method ScoreValue()
        Return 50
    End Method

    Method OnDestroyed()
        CreateSmallExplosion Self.entity
        FreeEntity Self.entity
    End Method
End Type

A Turret implements the same contract while retaining unrelated data and behaviour:

Type Turret Implements Enemy
    Field entity
    Field health
    Field rotation#
    Field fireCooldown#

    Method Update(world:GameWorld, dt#)
        Self.rotation = Self.rotation + 30.0 * dt
        RotateEntity Self.entity, 0, Self.rotation, 0

        Self.fireCooldown = Self.fireCooldown - dt
        If Self.fireCooldown <= 0
            FireSpread Self.entity
            Self.fireCooldown = 2.0
        EndIf
    End Method

    Method ApplyHit(damage)
        Self.health = Self.health - damage
    End Method

    Method IsDead()
        Return Self.health <= 0
    End Method

    Method CollisionEntity()
        Return Self.entity
    End Method

    Method ScoreValue()
        Return 100
    End Method

    Method OnDestroyed()
        CreateMediumExplosion Self.entity
        FreeEntity Self.entity
    End Method
End Type

One registry and one set of systems

An Interface does not make Each Enemy iterate unrelated concrete Type lists. A typed Enemy List provides one explicit membership set without a wrapper Type:

Global enemies:Enemy List = New List

Function AddEnemy(enemy:Enemy)
    enemies.Add(enemy)
End Function

The update system now depends on the Enemy contract rather than every implementing Type:

Function UpdateEnemies(world:GameWorld, dt#)
    For enemy:Enemy = Each enemies
        enemy.Update(world, dt)
    Next
End Function

Damage routing likewise becomes one operation:

Function ApplyEnemyHit(entity, damage)
    For enemy:Enemy = Each enemies
        If enemy.CollisionEntity() = entity
            enemy.ApplyHit(damage)
            Return
        EndIf
    Next
End Function

Cleanup and scoring no longer contain a branch per enemy Type. Typed List traversal permits safe removal of the current enemy. The List releases its stored reference; object deletion remains explicit:

Function RemoveDeadEnemies()
    For enemy:Enemy = Each enemies
        If enemy.IsDead()
            AddScore enemy.ScoreValue()
            enemy.OnDestroyed()
            enemies.Remove(enemy)
            Delete enemy
        EndIf
    Next
End Function

The removal order matters: remove the entry first, then Delete the enemy. Remove and Clear never delete contained objects. At shutdown, remove and delete remaining enemies before deleting the List container:

For enemy:Enemy = Each enemies
    enemies.Remove(enemy)
    enemy.OnDestroyed()
    Delete enemy
Next
Delete enemies

When enemy contains a Turret, enemy.Update(world, dt) selects Turret.Update. When it contains a Drone, the same source call selects Drone.Update. Calls through a concrete Drone or Turret variable remain statically dispatched.

Adding a new enemy

A Kamikaze Type can provide the existing contract without changing the update, damage, cleanup, scoring, or registry Types:

Type Kamikaze Implements Enemy
    Field entity
    Field health
    Field speed#

    Method Update(world:GameWorld, dt#)
        PointEntity Self.entity, world.playerEntity
        MoveEntity Self.entity, 0, 0, Self.speed * dt

        If EntityDistance(Self.entity, world.playerEntity) < 1.5
            DamagePlayer 25
            Self.health = 0
        EndIf
    End Method

    Method ApplyHit(damage)
        Self.health = Self.health - damage
    End Method

    Method IsDead()
        Return Self.health <= 0
    End Method

    Method CollisionEntity()
        Return Self.entity
    End Method

    Method ScoreValue()
        Return 75
    End Method

    Method OnDestroyed()
        CreateSmallExplosion Self.entity
        FreeEntity Self.entity
    End Method
End Type

The wave factory must still choose which concrete object to create. That choice is genuine and belongs at construction:

Select waveEnemyKind
    Case ENEMY_DRONE
        AddEnemy CreateDrone(x, y)
    Case ENEMY_TURRET
        AddEnemy CreateTurret(x, y)
    Case ENEMY_KAMIKAZE
        AddEnemy CreateKamikaze(x, y)
End Select

Interfaces eliminate repeated Type choices during every frame; they do not pretend that concrete Types never need to be chosen.

Use interfaces selectively

This pattern is valuable for a modest number of behaviourally different objects such as enemies, weapons, game states, and controllers. Thousands of identical bullets or particles are usually better represented by direct, concrete data collections.

The intended dependency is simple: without interfaces, every game system knows every enemy Type. With interfaces, each game system knows Enemy, and each concrete enemy Type knows how to fulfil that contract. The design does not require an EnemyEntry wrapper, interface inheritance, default implementations, reflection, overloads, generics, access modifiers, or a general class hierarchy.