Sunday, August 16, 2026

How Technozomians were created

How to create a scroller game?

By Srdjan Janjin

Here is how Technozomians were created; it is relatively simple and good for getting started with understanding game creation logic.



Introduction

Technozomians — A 1980s-Style Space Shooter for Android

Technozomians is a 2D space-shooter game developed in 2016, inspired by the scrolling arcade and home-computer games of the 1980s.

The project started partly as an experiment. Android Studio was still relatively new, smartphones were becoming powerful enough to run much more sophisticated applications than the phones of the previous generation, and I wanted to see what could be done with them.

The idea was not to create a commercial game studio or a large production. It was primarily a technical and creative experiment — to take the kind of scrolling shooter that I remembered from the 1980s and build one from scratch for a modern mobile phone.

The game was written in Java using the LibGDX framework, a cross-platform Java game development framework that provides access to graphics, input, audio, file handling, and other low-level game-development functionality while leaving the actual game logic to the developer.

A $0 Game

The development cost of the game was $0.

Not because the game took no effort — quite the opposite. It took many, many hours of development, experimentation, drawing, testing and searching for suitable resources.

The project was built using free development tools and freely available resources. Graphics, sounds, music, fonts and other resources were either created using free tools or obtained from sources offering resources that could legally be used under their respective licenses.

A collection of useful resource websites and tools used during development is included at the end of the article. Since websites and licensing terms can change over time, the original links should always be checked against the current license before using any of those resources in a new project.

The intention was to demonstrate that a complete game could be created without a budget for commercial development tools or professionally produced assets. The main investment was time and curiosity.

From the ZX Spectrum to Android

The idea behind the project goes back much further than Android.

In 1988, I wrote a 3D wireframe program in BASIC for the ZX Spectrum as part of my school graduation project. Years later, when smartphones became powerful enough to handle real-time graphics, I became curious about how the same kind of mathematical and graphical techniques would perform on a mobile device.

Technozomians was one of the results of that curiosity.

The game contains ten increasingly difficult levels, limited ammunition, destructible and indestructible terrain, enemies, rockets, power-ups, clouds, rotating gears, explosions, sound effects, and music.

The player has only one life. When the ship is destroyed, the current game ends and the player returns to the menu. However, successfully completed levels remain unlocked, allowing the player to continue from the furthest level reached.

Although the game is relatively small by modern standards, it contains many of the fundamental concepts found in much larger games: a game loop, entity management, collision detection, animation, sprite rendering, sound, input handling, level progression, score and high-score management, resource loading, camera and viewport management, and platform-specific Android configuration.

The following sections describe how those pieces were put together and how the game actually works internally.


Technozomians — How the Game Works and How It Was Built

A Short Introduction

Technozomians is a 2D side-scrolling space shooter developed with the LibGDX framework.

The basic idea is simple: the player controls a spaceship flying from left to right through a sequence of increasingly difficult levels. The player has only one life. If the ship is destroyed, the game returns to the menu.

There are ten levels in the game. Completing a level unlocks the next one, so after losing a life the player does not have to start from the beginning of the game. The highest level reached is remembered and can be selected again from the level-selection screen.

The game deliberately uses relatively simple mechanics, but combines them to make the levels difficult:

  • The environment continuously scrolls from right to left.

  • Terrain can enter the screen from above and below rather than simply appearing instantly.

  • Some terrain can be destroyed by shooting it; other parts are indestructible.

  • Ammunition is limited and must be collected during the level.

  • A special power-up allows the player to fire faster.

  • Clouds can obscure the player or enemies, but cannot cause damage.

  • Gears can be destroyed, but require several shots.

  • Some enemies have a visible engine flame which can also change their movement speed.

  • Rockets and other enemies are placed at predefined positions in each level.

  • Explosions, sound effects, and music provide feedback to the player.

Although the game looks like a conventional 2D shooter, much of its behaviour comes from a relatively small set of reusable concepts.


The Main Game Class — SpaceInvaders.java

The central class of the game is SpaceInvaders.java.

It is responsible for initializing the resources used by the game and coordinating the main game loop. This includes loading graphical assets, fonts, and audio resources, preparing the rendering environment, and then repeatedly updating and drawing the game.

The application starts by loading the resources required by the game:

fontTexture = new Texture(
Gdx.files.internal("font.png")
);

Similarly, the game's graphical assets are loaded and made available to the different game components.

Audio resources are initialized in the same central location:

menuMusic = Gdx.audio.newMusic(
Gdx.files.internal("bwv851.mid")
);

LaserSound = Gdx.audio.newSound(
Gdx.files.internal("raygun.wav")
);

Boom = Gdx.audio.newSound(
Gdx.files.internal("Boooom.wav")
);

This means that the individual game objects do not need to repeatedly load their own resources from disk. The resources are prepared centrally and then used during the game.

The class also contains the main rendering/update cycle.

Conceptually, the application follows this sequence:

Application starts
Load game resources
Initialize game objects
Main loop
+----------------+
| |
| Update state |
| |
+-------↓--------+
|
Calculate positions
|
Check collisions
|
Update animations
|
Select sprites
|
+-------↓--------+
| |
| Render frame |
| |
+-------↓--------+
|
Display completed frame
|
└──────→ Next frame

This makes SpaceInvaders.java effectively the orchestrator of the game.

The more specialized classes perform specific tasks. For example:

SpaceInvaders
├── EntityManager
│ ├── enemies
│ ├── terrain
│ ├── bullets
│ ├── clouds
│ └── power-ups
├── Player
├── Texture / Sprite resources
├── Font
└── Audio

This separation is useful because the central class controls the overall application flow, while the individual classes contain the behaviour of particular parts of the game.

Resource Loading vs. Game Logic

One interesting aspect of the original implementation is that resource initialization and the game loop are located together in the main game class.

In a modern project, these responsibilities might be divided further into dedicated managers or services:

AssetManager
TextureManager
AudioManager
FontRenderer
Game
EntityManager

The original implementation is simpler. For a game of this size, having a single central class responsible for initializing the resources and coordinating the main loop is perfectly practical.

It also makes the execution flow relatively easy to understand:

Start the game → load everything → initialize the world → update everything → render everything → repeat.



SpaceInvaders.java
┌─────────────┼─────────────┐
↓ ↓ ↓
Resources Game Loop Game State
│ │
┌───────┼───────┐ │
↓ ↓ ↓ ↓
Sprite Font Audio EntityManager
┌────────────┼────────────┐
↓ ↓ ↓
Player Enemies Terrain
Collision
State changes
Rendering

Levels as Text

One of the more interesting aspects of the implementation is how levels are defined.

Instead of creating a separate level file or using a graphical level editor, the terrain and objects of a level are encoded as characters in strings.

For example, a level is represented by an array:

Matrica = new String[10];

Each string represents one horizontal row of the level.

A simplified example might look like:

0000000000000000A000000000000000
0000000000000000000000000000000
000000000J000000000000000000000
0000000000000000000000000000000
0000000000000000000000000000000
0000000000000000000000000000000
0000000000000000000000000000000
0000000000000000000000000000000
0000000000000000000000000000000
EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE

The actual game uses much longer strings, but the principle is the same.

Different characters represent different objects.

For example:

J  ammunition
H  octagon
G  downward obstacle
F  obstacle
D  rocket
E  static wall
1  enemy
3  power-up
9  cloud
A  gear

The level therefore becomes a kind of ASCII-style level map.

The advantage of this approach is simplicity. A level can be modified by changing characters in a string instead of writing new Java code.


Scrolling the Level

The level is not created on the screen all at once.

The game keeps a counter:

private int BrojacNivoa = 0;

As the game progresses, this counter advances through the level strings.

Conceptually:

             scrolling direction →

000000000000000000000000000000000000000000000
0000000000A000000000000000000000000000000000
00000000000000000000000000000000000000000000
00000000000000000000000000000000000000000000
EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE

The game reads one column of characters at a time and creates entities corresponding to the characters found in that column.

This means the level behaves more like a stream of objects than a static map.

The relevant code follows this basic idea:

BrojacNivoa++;

if (BrojacNivoa < 130) {
    for (int j = 0; j < Matrica.length; j++) {
        char c = Matrica[j].charAt(BrojacNivoa);

        // create the appropriate entity
    }
}

This is a very lightweight way of implementing a scrolling level.


Terrain Appearing from Above and Below

An important visual feature is that terrain does not simply appear at its final position.

Some objects enter the screen from the top or bottom and move toward a predefined position.

The code distinguishes between objects above and below the centre of the screen:

if (y > spaceInvaders.height / 2) {
    // object approaches from above
    pravac = -1;
} else {
    // object approaches from below
    pravac = 1;
}

The object then moves toward its target position.

This produces the effect of terrain gradually emerging from the edge of the screen.

The result is much more dynamic than simply drawing a wall at a fixed location.


A Common Entity Model

The game uses an Entity concept as the basis for many different objects.

An entity has properties such as position, direction, texture, and behaviour.

More specialised objects inherit or build upon this basic concept.

For example, enemies contain properties such as:

public int UpucavaSe;
public int BrojUpucavanja;
public int MaxBrojUpucavanja;

public boolean DozvoliMlaz;
public boolean MlazUklj;

The names are Serbian because this was the original development code, but their meaning is straightforward:

UpucavaSe          Can this object be shot?
BrojUpucavanja     How many times has it been hit?
MaxBrojUpucavanja  How many hits can it take?
DozvoliMlaz        Is an engine flame allowed?
MlazUklj           Is the engine flame currently active?

This allows many different objects to be handled using the same general entity system.


5. Destructible and Indestructible Objects

Not everything in the environment behaves in the same way.

Some objects cannot be destroyed:

UpucavaSe = 0;

Others can be destroyed:

UpucavaSe = 1;

The system can also specify how many shots an object requires.

For example:

entities[prvi].BrojUpucavanja = 0;
entities[prvi].MaxBrojUpucavanja = 3;

This means the object starts with zero hits and can take three hits before being destroyed.

This mechanism is used for several different objects in the game.

It also makes it possible to create more difficult levels without writing a completely different collision system.


The Gear Mechanism

The gears are a good example of how several simple mechanics can be combined.

The gear can be shot multiple times. The large gear is destroyed first, while the smaller gear remains.

The rendering logic contains a condition similar to:

if (BrojUpucavanja < 3) {
    // draw the large gear
}

The gears also rotate, giving the otherwise static obstacle some visual movement.

The result is an obstacle which is not simply:

"Shoot once → destroy."

Instead, it becomes a small gameplay puzzle:

"Shoot the large gear enough times, then deal with what remains."


Limited Ammunition

The player does not have unlimited ammunition.

At the beginning of a level, ammunition is calculated according to the current level:

Municija = 100 + spaceInvaders.Nivo * 10;

Every shot consumes ammunition:

Municija--;

Ammunition can also be collected during the level.

When the appropriate object is collected, the player receives additional ammunition:

Municija += 400;

This changes the way the player approaches a level.

Shooting everything is not necessarily a good strategy. Ammunition has to be conserved because eventually it can run out.


Power-Ups

There is also a special box which changes the player's firing behaviour.

When the player collects it, the game changes the ammunition/firing state:

BrojMetaka = 12;

This provides a temporary or special rapid-fire capability.

The important point is that power-ups are implemented using the same general entity mechanism as other objects.

The level designer only needs to place the appropriate character in the level map.


Clouds Are Obstacles, But Not Enemies

Clouds have a different role.

They do not damage the player.

Instead, they interfere with visibility.

The player can move behind a cloud and temporarily disappear from view. An enemy can also be hidden behind one.

This is a good example of a gameplay mechanic which does not need complicated physics.

The cloud simply changes what is visible.

That creates uncertainty without introducing another damage mechanism.


Engine Flames Can Change Enemy Speed

Some enemies have an engine flame.

The flame is not only decorative.

The enemy has properties such as:

DozvoliMlaz
MlazUklj

When the engine flame is activated, the movement direction/speed is modified.

The code contains logic along the lines of:

if (MlazUklj) {
    direction.x = direction.x - .4f;
}

Therefore, the engine flame actually changes the enemy's movement.

This is a nice example of using a visual effect to represent a real gameplay state.

The player sees the flame and can infer:

"That enemy is accelerating."


Level Progression

The game has ten levels.

When a level is completed:

spaceInvaders.Nivo++;

if (spaceInvaders.Nivo > spaceInvaders.MaxNivo)
    spaceInvaders.MaxNivo = spaceInvaders.Nivo;

Nivo represents the current level.

MaxNivo represents the highest level the player has unlocked.

This creates the progression system:

Level 1
   ↓
Level 2 unlocked
   ↓
Level 3 unlocked
   ↓
...
   ↓
Level 10

If the player dies, the game returns to the menu, but the unlocked level is retained.

This is a very simple alternative to a traditional save-game system.


One Life Changes the Game

The player has only one life.

There is no complicated health system where the player can absorb ten hits and regenerate.

Once the player's ship is destroyed, the current run ends.

The player returns to the menu and chooses the level again.

Combined with limited ammunition and difficult terrain, this makes the game deliberately unforgiving.

The player has to learn the level rather than simply survive through accumulated health.

Death Animation and Game State Transition

When the player's ship is hit, the ship does not disappear immediately. Instead, the game enters a short death sequence.

The player's ship continues to rotate while the explosion animation is played. At the same time, the screen gradually fades to black. Only after this sequence has finished does the game return to the menu.

Conceptually:

Player is hit
Death state
Ship continues rotating
Explosion animation
Screen gradually fades
Death sequence finished
Return to menu

This is a small detail, but it makes a significant difference to the feel of the game. An immediate transition:

player.destroy();
showMenu();

would be technically sufficient, but visually very abrupt.

Instead, the game separates the event that causes death from the transition to the next game state. The player is already doomed, but the game gives the event time to be seen and understood.

That same principle is used throughout game development: an event does not necessarily have to cause an immediate change of screen or state. It can initiate an animation/state transition that takes place over several frames.



Collision and Object Behaviour

The game essentially treats gameplay as interactions between entities.

A simplified conceptual model is:

Player
   |
   +-- fires --> Bullet
                   |
                   +-- hits Enemy
                   |      |
                   |      +-- reduce hit count
                   |      +-- destroy?
                   |
                   +-- hits Terrain
                          |
                          +-- destructible?

The same general mechanism can therefore handle many different objects.

For example:

if (enemy.UpucavaSe == 1) {
    enemy.BrojUpucavanja++;

    if (enemy.BrojUpucavanja >= enemy.MaxBrojUpucavanja) {
        // destroy entity
    }
}

The actual implementation is more involved, but the underlying idea is straightforward.

Score and High Score

The game also keeps track of the player's Score.

Destroying an enemy increases the score, so shooting enemies is not only necessary for survival — it also directly contributes to the player's final result.

The game also maintains a High Score, giving the player a reason to replay levels even after they have already been unlocked.

Conceptually:

Enemy destroyed
Score += points
Compare with High Score
Save new High Score if necessary

This creates an additional layer of progression:

Level progression Score progression
↓ ↓
Unlock level 2 Improve score
Unlock level 3 Beat High Score
... ...
Unlock level 10 Set a new record

This is particularly appropriate for a game with only one life. Even if the player has already unlocked a level, there is still a reason to play it again: try to survive longer and beat the previous score.

And there's a nice connection here with the way you designed the levels. Since ammunition is limited, the player has to make decisions about what to shoot. Some objects are obstacles that have to be destroyed, while others can be ignored. Therefore, score, ammunition and survival are competing considerations rather than independent mechanics.


Assets and Rendering

The game uses LibGDX for rendering.

Textures are loaded from the application's assets:

Gdx.files.internal("raygun.wav")
Gdx.files.internal("Boooom.wav")

Graphical assets are managed through the game's texture manager.

The project contains separate assets for things such as:

player
enemies
bullets
explosions
clouds
gears
terrain
engine flames
power-ups
fonts

This separates the actual game logic from the graphical resources.


Sound and Music

The original project also contains both music and sound effects.

For example:

menuMusic = Gdx.audio.newMusic(
    Gdx.files.internal("bwv851.mid"));

Sound effects are loaded separately:

LaserSound = Gdx.audio.newSound(
    Gdx.files.internal("raygun.wav"));

Boom = Gdx.audio.newSound(
    Gdx.files.internal("Boooom.wav"));

The music can be looped:

menuMusic.setLooping(true);
menuMusic.play();

The project therefore treats music and short sound effects differently, using LibGDX's Music and Sound APIs.


Why the Level Representation Is Interesting

Looking back at the implementation, one of the most interesting design decisions is how little information is required to describe a level.

A character in a string can effectively mean:

"Create an entity of type X at this position."

For example:

A → gear
J → ammunition
D → rocket
9 → cloud
3 → power-up

This means the actual level data is almost entirely separated from the game mechanics.

The code knows what A means.

The level designer only needs to decide where to put A.

That is a simple form of data-driven game design.


The Overall Architecture

At a high level, the game can be viewed like this:

                    LEVEL DATA
                 String[10] matrix
                       |
                       v
                EntityManager
                       |
             creates entities
                       |
        +--------------+--------------+
        |              |              |
      Player         Enemies       Terrain
        |              |              |
        +-------+------+--------------+
                |
             Collision
                |
        +-------+--------+
        |                |
      Destroy          Collect
        |                |
    Explosion        Power-up
        |
      Score

The game loop repeatedly updates the entities and renders them.

Conceptually:

Read input
    ↓
Update player
    ↓
Create / move bullets
    ↓
Move enemies
    ↓
Scroll level
    ↓
Check collisions
    ↓
Apply damage / collect items
    ↓
Create explosions
    ↓
Play sounds
    ↓
Render everything
    ↓
Next frame

This is essentially the same basic structure used by much more sophisticated games.

The difference is that here it is implemented with relatively simple Java classes and LibGDX primitives.


Double Buffering — Drawing One Screen While Showing Another


The game uses two rendering surfaces (two frame buffers).

At any given moment, one buffer is visible to the player while the other is used to construct the next frame.

The process is essentially:

VISIBLE SCREEN
|
Display buffer
|
swap buffers
|
Drawing buffer
|
clear screen
|
+-----------+-----------+
| | |
Player Enemies Terrain
| | |
+-----------+-----------+
|
calculate new state
|
select current sprite
|
draw

At the beginning of each frame, the hidden drawing surface is cleared.

The game then processes its entities one by one. For each object, it calculates its new position and determines what should be displayed.

For example, an enemy that was hit by a bullet may no longer be rendered as an enemy. Instead, its state changes and an explosion animation is selected.

Conceptually:

clearBackBuffer();

for (Entity entity : entities) {

entity.update();

Sprite sprite = entity.getCurrentSprite();

sprite.setPosition(
entity.getX(),
entity.getY()
);

sprite.draw(backBuffer);
}

swapBuffers();

The actual implementation is more specific to LibGDX, but the principle is the same.

The important part is that the player never sees the intermediate drawing process.

Instead:

Frame N is being displayed
Frame N+1 is constructed off-screen
All entities are updated
Sprites and effects are drawn
Frame N+1 becomes visible

This is known as double buffering.

It is particularly useful in a game because many objects may change simultaneously:

  • the player's position,
  • enemy positions,
  • scrolling terrain,
  • bullets,
  • explosions,
  • clouds,
  • rotating gears,
  • engine flames,
  • score and other UI elements.

Without this separation, the player could potentially see parts of the scene being drawn at different times, producing flickering or visual artefacts.

Updating the Scene

The important conceptual distinction is that the game does not simply redraw the same picture.

Every frame is effectively a new calculation:

Previous state
Update all entities
Calculate new positions
Check collisions
Change entity states
Select appropriate sprites
Draw complete new frame
Display it

So when an enemy is hit, for example, the next frame might contain an explosion sprite instead of the enemy sprite. The same mechanism can be used for the player's death animation, disappearing terrain, power-ups, rotating objects, and other effects.



Looking Back

The interesting thing about this project is that it does not rely on a sophisticated game engine or a complicated level editor.

Instead, it combines several simple ideas:

  • entities

  • properties

  • text-based level descriptions

  • scrolling

  • collision detection

  • simple state changes

  • sprite animation

  • sound effects

  • basic progression

The game therefore demonstrates an important principle of game development:

Complex behaviour does not necessarily require complex individual components.

A destructible wall, a gear, a cloud, ammunition and a power-up can all be represented as variations of the same basic concept: an entity with a position, appearance and set of properties.

The result is a game which is considerably more complex than its individual building blocks suggest.

And perhaps the most interesting part is that the level itself is essentially just text.

A long string of characters becomes a scrolling 2D world once the game interprets those characters as objects.


Possible Optimization: Spatial Partitioning for Collision Detection

The original version of the game uses a deliberately simple approach to collision detection. For every active bullet, the game checks the bullet against every active entity that can potentially be hit.

A simplified version of the original approach looks like this:

for (Bullet bullet : bullets) {

    if (!bullet.isActive())
        continue;

    for (Entity entity : entities) {

        if (!entity.isActive())
            continue;

        if (!entity.canBeShot())
            continue;

        if (entity.getBounds().overlaps(bullet.getBounds())) {
            handleCollision(bullet, entity);
        }
    }
}

For a relatively small number of objects, this is perfectly reasonable. The collision test itself is very cheap, and the game never had hundreds or thousands of simultaneously active objects.

However, there is an interesting optimization that could be applied if the number of objects became much larger.

The Problem with Pairwise Collision Testing

Suppose there are N objects that can potentially collide.

If every object has to be compared with every other object, the number of possible pairs grows approximately as:

[
\frac{N(N-1)}{2}
]

This is O(N²) complexity.

For example:

10 objects       →      45 possible pairs
100 objects      →   4,950 possible pairs
1,000 objects    → 499,500 possible pairs

The important point is that most of these tests are usually pointless.

A bullet on the left side of the screen cannot possibly collide with an enemy on the far right during the current frame.

Yet a naive collision algorithm may still test the pair.


Spatial Grid

One way to reduce the number of unnecessary tests is to divide the game world into a grid of smaller cells.

For example:

+-------+-------+-------+-------+
|       |       |       |       |
|   A   |       |   B   |       |
|       |   C   |       |       |
+-------+-------+-------+-------+
|       |       |       |       |
|       |   D   |       |   E   |
|       |       |       |       |
+-------+-------+-------+-------+
|   F   |       |       |       |
|       |       |   G   |       |
|       |       |       |       |
+-------+-------+-------+-------+

Each object is assigned to the cell in which it currently resides.

Instead of asking:

"Can this bullet collide with any object anywhere on the screen?"

we can first ask:

"Which objects are near this bullet?"

Only those objects become collision candidates.


Building the Grid

A simple uniform grid can be implemented using a two-dimensional array or a collection of cells.

Conceptually:

for (Entity entity : entities) {

    int cellX = (int)(entity.x / CELL_SIZE);
    int cellY = (int)(entity.y / CELL_SIZE);

    grid[cellX][cellY].add(entity);
}

For example, if the cell size is 100 pixels:

x = 350
y = 240

cellX = 350 / 100 = 3
cellY = 240 / 100 = 2

The entity therefore belongs to:

grid[3][2]

The grid is rebuilt or updated as objects move.


Checking Only Nearby Objects

Now consider a bullet located here:

+-------+-------+-------+
|       |       |       |
|       |       |       |
+-------+-------+-------+
|       |   B   |       |
|       |       |       |
+-------+-------+-------+
|       |       |       |
|       |       |       |
+-------+-------+-------+

Instead of checking every entity in the game, we first look at the bullet's cell.

Depending on the size of the objects, we may also need to check neighbouring cells:

+-------+-------+-------+
|   X   |   X   |   X   |
+-------+-------+-------+
|   X   |   B   |   X   |
+-------+-------+-------+
|   X   |   X   |   X   |
+-------+-------+-------+

The X cells contain the only objects that are considered possible collision candidates.

Objects much farther away are ignored.


Collision Test Using the Grid

The collision code could then look conceptually like this:

for (Bullet bullet : bullets) {

    if (!bullet.isActive())
        continue;

    int cellX = getCellX(bullet.x);
    int cellY = getCellY(bullet.y);

    for (Entity entity : getNearbyEntities(cellX, cellY)) {

        if (!entity.isActive())
            continue;

        if (!entity.canBeShot())
            continue;

        if (bullet.getBounds().overlaps(entity.getBounds())) {
            handleCollision(bullet, entity);
        }
    }
}

The important difference is that getNearbyEntities() does not return every entity in the game.

It returns only objects occupying the relevant cells.


Why This Helps

Imagine a game containing 1,000 objects.

A naive implementation potentially has to consider hundreds of thousands of object pairs.

With spatial partitioning, each object may only have a handful of nearby candidates.

Instead of:

Object
   ↓
check against
   ↓
ALL objects

we get:

Object
   ↓
find its grid cell
   ↓
find nearby cells
   ↓
check only nearby objects

The actual performance depends on the distribution of objects and the choice of cell size, but the reduction in unnecessary collision tests can be substantial.


Choosing the Cell Size

The cell size matters.

If the cells are too large:

+-----------------------------+
|                             |
|        many objects         |
|                             |
+-----------------------------+

then each cell contains many objects and we gain little.

If the cells are too small:

+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| | | | | | | | | | | | | | |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

then objects may occupy multiple cells and the overhead of maintaining the grid increases.

A useful starting point is often a cell size comparable to the typical size of the objects being tested.

For a game such as Technozomians, where most objects are relatively small and the world is a simple 2D screen, a uniform grid would be a natural choice.


An Important Detail: Objects Can Cross Cell Boundaries

An object is not necessarily contained completely inside one cell.

For example:

+-------+-------+
|       |       |
|    +--------+ |
|    | Object | |
|    +--------+ |
|       |       |
+-------+-------+

If an object is large enough to overlap two or more cells, it needs to be registered in all relevant cells, or the collision system needs to take its bounding box into account when determining which cells to search.

This is one reason why a spatial grid adds complexity compared with the original simple implementation.


Why the Original Implementation Was Reasonable

For Technozomians, the straightforward implementation was probably the better engineering decision.

The game normally has a relatively small number of active objects:

Player
+ bullets
+ enemies
+ terrain
+ clouds
+ power-ups
+ explosions
+ gears
...

The collision operation itself is inexpensive, and the game was running comfortably on the target hardware.

Introducing a spatial grid would therefore have added:

  • additional data structures,
  • additional bookkeeping,
  • handling of objects crossing cells,
  • more complicated debugging,

without providing a noticeable benefit.

This is a good example of an important optimization principle:

Do not optimize an algorithm simply because a theoretically faster algorithm exists. Optimize when the existing algorithm is actually a bottleneck.

For a small number of entities, a simple O(N²) collision check can be entirely adequate.


From Pairwise Testing to Spatial Partitioning

The two approaches can be summarized as follows:

Original approach

             ALL OBJECTS
                  │
                  ▼
        +-------------------+
        | Test every pair   |
        +-------------------+
                  │
                  ▼
             Collision?

Spatial-grid approach

             ALL OBJECTS
                  │
                  ▼
             Place into
             grid cells
                  │
                  ▼
          Find nearby cells
                  │
                  ▼
        Test nearby objects
                  │
                  ▼
             Collision?

The second approach does not make the actual collision test itself faster. Instead, it makes the much more important change of reducing the number of collision tests that need to be performed in the first place.

This distinction is important. A simple rectangle-overlap test such as:

a.getBounds().overlaps(b.getBounds())

is already very cheap. The expensive part, when the number of objects becomes large, is performing that test hundreds of thousands of times for pairs of objects that are nowhere near each other.


A Natural Next Step

A spatial grid is only one possible solution.

More sophisticated games may use:

  • spatial hashing,
  • quadtrees,
  • bounding volume hierarchies,
  • sweep-and-prune,
  • physics engines with their own broad-phase collision detection.

The general principle is the same:

First eliminate objects that cannot possibly collide; then perform the accurate collision test only on the remaining candidates.

In the original Technozomians implementation, the simpler approach was sufficient. But the game provides a good example of where the next level of optimization would naturally appear if the number of entities increased significantly.


LibGDX

Technozomians was built using LibGDX, a Java-based framework for game development.

LibGDX is not a complete game engine in the sense that it does not provide the whole game design or gameplay logic automatically. Instead, it provides a common set of low-level and mid-level tools for handling the parts of a game that would otherwise have to be implemented separately for each platform.

In this game, LibGDX is mainly used for:

  • 2D graphics and rendering
  • textures and sprites
  • animation
  • keyboard and touch input
  • audio and music
  • file/resource access
  • timing and frame updates
  • basic mathematical utilities
  • Android application integration

For example, loading a texture is done through the LibGDX API:

Texture texture = new Texture(
Gdx.files.internal("player.png")
);

The same Gdx interface is used for accessing other platform-independent functionality.

Rendering

The game uses LibGDX's 2D rendering functionality to draw sprites and other graphical elements.

A typical operation is conceptually:

sprite.setPosition(x, y);
sprite.draw(batch);

The game therefore does not need to know the low-level details of how Android actually sends pixels to the display.

LibGDX handles that part.

Input

The framework also provides a common input interface.

For example:

Gdx.input.isTouched()

allows the game to determine whether the touchscreen is being touched without having to deal directly with Android's native touch-event system.

This was particularly useful because the same game logic could be used without writing separate rendering and input code specifically for Android.

Audio

The game uses LibGDX for both music and short sound effects:

Music music = Gdx.audio.newMusic(
Gdx.files.internal("bwv851.mid")
);

Sound laser = Gdx.audio.newSound(
Gdx.files.internal("raygun.wav")
);

The distinction between Music and Sound is useful because background music and short effects have different playback requirements.

Mathematics and Geometry

LibGDX also provides utility classes used by the game for things such as vectors, rectangles and random numbers.

For example, collision detection can use:

if (entity.getBounds().overlaps(
bullet.getBounds())) {

// collision
}

The game does not need to implement rectangle intersection mathematics itself.

Similarly, vectors can represent position and movement:

Vector2 direction;

File and Asset Access

Game resources are stored in the application's assets and accessed through LibGDX:

Gdx.files.internal("font.png");
Gdx.files.internal("raygun.wav");

This provides a platform-independent way of accessing resources.


What LibGDX Does — and What It Doesn't

An important distinction is that LibGDX provides the tools, but the game itself is still written by the developer.

LibGDX provides:

Rendering
Input
Audio
Textures
Sprites
Math utilities
File access
Platform integration

while the game provides:

Game rules
Levels
Enemies
Player behaviour
Collision rules
Scoring
Ammunition
Power-ups
Enemy AI
Animations
Game states
Level progression

So the relationship can be summarized as:

Technozomians
┌──────────┴──────────┐
│ │
Your code LibGDX
│ │
│ ┌──────┼──────┐
│ │ │ │
Game logic Graphics Input Audio
│ │ │ │
Levels Textures Touch Music
Enemies Sprites Sound
Collision Rendering
Score
Rules

This is one of the reasons LibGDX was attractive for projects like this. It removed much of the platform-specific plumbing while leaving the developer in control of the actual game.

For Technozomians, LibGDX is therefore best thought of as the layer between the Java game code and the underlying Android/graphics/audio system.

And given when you made the game, this was quite significant: instead of writing directly against the Android graphics and input APIs, you could concentrate on making the game itself.


Handling Different Screen Sizes and Resolutions

One of the important things LibGDX provides is an abstraction layer between the game and the physical hardware of the device.

Android devices can have very different:

  • screen resolutions,
  • screen sizes,
  • aspect ratios,
  • pixel densities,
  • orientations.

A game should not depend on the assumption that every device has, for example, exactly 800 × 480 pixels.

Instead, the game can work with a virtual game coordinate system and let LibGDX map that coordinate system to the actual screen.

For example, the game can conceptually work with:

Virtual game world
800 × 480

while the actual device might have:

Device A 800 × 480
Device B 1280 × 720
Device C 1920 × 1080
Device D 2560 × 1440

The game logic can continue to use the same coordinates:

player.setPosition(400, 240);

while the rendering system takes care of transforming those coordinates to the physical display.

This is particularly important for touch input.

Suppose the player ship is displayed at:

+-------------+
| |
| SHIP |
| |
+-------------+

The physical touchscreen reports a touch position in screen coordinates.

The game, however, needs to know where that touch occurred in its own virtual coordinate system.

LibGDX's camera and viewport system can perform the appropriate transformation:

Physical screen coordinates
Viewport
Camera transformation
Virtual game coordinates
Game object / sprite

So a touch that physically occurs at, for example:

screen: (1375, 620)

can be converted to something like:

game: (572, 310)

depending on the screen size, viewport, and camera configuration.

The important consequence is that the same game code can work on different devices without manually creating separate coordinate systems for every screen resolution.

This is especially important for a touch-controlled game. If a player touches the screen over a particular sprite, the game needs to interpret that touch as occurring over the same object in the game's coordinate system.

A simplified representation is:

Physical device
+---------------------+
| |
| SHIP |
| ↑ |
| TOUCH |
| |
+---------------------+
│ coordinate transformation
Virtual game world
+----------------+
| |
| SHIP |
| ↑ |
| (x,y) |
| |
+----------------+

This abstraction is one of the reasons frameworks such as LibGDX are useful for mobile games: the developer can concentrate on the game's coordinate system and interaction model instead of writing separate scaling and input-conversion code for every possible Android device.

In Technozomians, this principle is especially important because the game uses both screen rendering and touch interaction. The player should see an object in one location and be able to interact with that same location regardless of the physical resolution of the device.


Orthographic Camera and Virtual Viewport

One of the less visible, but important, parts of the game is the way it handles different screen sizes and resolutions.

The game does not use the physical screen dimensions directly for its game logic. Instead, it defines a virtual coordinate system and uses an orthographic camera together with a virtual viewport to map that coordinate system onto the actual device screen.

The project contains two classes specifically for this purpose:

OrthoCamera
VirtualViewport

The basic idea is:

GAME WORLD
Virtual coordinates
800 × 480
OrthoCamera
VirtualViewport
Physical device
1280 × 720 / 1920 × 1080
/ other resolution

Orthographic Camera

The game uses an orthographic camera rather than a perspective camera.

This is appropriate for a 2D game because objects should not become smaller simply because they are farther away from the camera.

In an orthographic projection:

+--------------------------+
| |
| PLAYER |
| |
| |
| ENEMY |
| |
+--------------------------+

an object's apparent size is determined by the game's coordinate system rather than by its distance from the camera.

The camera therefore provides a consistent 2D coordinate space for the game.

A simplified example is:

OrthographicCamera camera =
new OrthographicCamera();

camera.setToOrtho(
false,
WORLD_WIDTH,
WORLD_HEIGHT
);

The important idea is that the game can work with its own dimensions instead of having to know the exact pixel dimensions of every Android device.


Virtual Viewport

The VirtualViewport class is responsible for connecting that virtual game space to the actual screen.

For example, the game might conceptually work in:

Virtual resolution:

800 × 480

while the phone could have:

Device A: 800 × 480
Device B: 1280 × 720
Device C: 1920 × 1080
Device D: 2560 × 1440

The viewport determines how the 800 × 480 game world is displayed on each of these screens.

The important thing is that the game logic can continue using the same coordinates.

For example:

player.x = 400;
player.y = 240;

means the same thing regardless of whether the physical display has 800 or 1920 horizontal pixels.


Keeping the Aspect Ratio

One of the problems with simply stretching an image to fill every screen is distortion.

Suppose the game world has an aspect ratio of:

800 / 480 = 1.667

but the phone has a very different aspect ratio.

If the entire game were stretched independently in both directions, a circular object could become elliptical:

Original Incorrect stretching

O OOO
O O O O
O OOO

A viewport can instead preserve the intended proportions.

Depending on the chosen viewport strategy, this may result in unused space at the edges of the screen rather than distorting the game world.

Conceptually:

+--------------------------------+
| |
| +------------------------+ |
| | | |
| | GAME WORLD | |
| | | |
| +------------------------+ |
| |
+--------------------------------+

The important point is that the game remains geometrically consistent.


Touch Coordinates

The camera and viewport are particularly important for touch input.

The touchscreen reports a position in physical screen coordinates.

The game needs that position in its own virtual coordinate system.

For example:

Physical screen:

1920 × 1080

Touch:
(1450, 620)

The game should not simply use (1450, 620) as the player's game coordinate.

Instead, the coordinate is transformed:

Physical touch
(1450, 620)
Viewport
Camera
Virtual game coordinate
(604, 276)

Now the game can determine which object is underneath the user's finger using the same coordinates it uses to draw that object.

This is particularly important when the game uses rectangular bounds for interaction and collision detection.

Conceptually:

Vector3 touch = new Vector3(
screenX,
screenY,
0
);

camera.unproject(touch);

float gameX = touch.x;
float gameY = touch.y;

The actual implementation can differ depending on the viewport and camera setup, but the principle is the same: convert between screen space and game space.


Why This Matters

Without this abstraction, a mobile game would have to deal with different devices separately:

Phone A
special coordinates

Phone B
different coordinates

Phone C
another coordinate system

Phone D
another one again

With a virtual game coordinate system:

Game logic
Virtual coordinates
+----------+----------+
│ │
Device A Device B
viewport viewport
│ │
▼ ▼
physical physical
screen screen

The game logic remains independent of the physical display.

This also makes the relationship between drawing and input much cleaner:

If an object is drawn at (x, y) in the game's coordinate system, a touch converted into that same coordinate system can be tested against the object's bounds.

That means the player does not have to know anything about the actual resolution of the phone. They simply touch the object they see.


Why an Orthographic Camera Was Appropriate Here

There is no need for perspective projection in Technozomians. The game is fundamentally a 2D side-scroller.

The camera therefore provides a stable 2D coordinate system:

Y
|
|
+------------→ X
(0,0)

Objects have positions in this world:

Player → (100, 220)
Enemy → (650, 300)
Rocket → (500, 150)

The camera and viewport then determine how these coordinates appear on the physical display.

This is one of those pieces of infrastructure that the player never notices — but without it, a game designed for one particular screen size could behave incorrectly on another device.


The Architecture

The overall relationship can therefore be summarized as:

GAME LOGIC
Virtual coordinates
+-----------+-----------+
│ │
▼ ▼
Rendering Input
│ │
▼ ▼
Orthographic Camera Screen coordinates
│ │
└──────────┬────────────┘
VirtualViewport
Physical display

This is a good example of what a framework such as LibGDX provides: it gives the developer the mathematical and rendering infrastructure needed to work with different devices, while the developer decides what coordinate system the game itself should use.


Layered Rendering and Parallax Scrolling


The game does not draw everything at the same depth. The scene is built in several visual layers, and the order in which those layers are rendered is important.

The background is drawn first, followed by a second, closer background layer. The two layers scroll at different speeds, creating a simple parallax scrolling effect.

Conceptually:

SCREEN
┌─────────────────────────────────────────────┐
│ │
│ Layer 1 — distant space │
│ ← ← ← slow │
│ │
│ Layer 2 — closer background │
│ ← ← ← ← faster │
│ │
│ Player Enemy │
│ 🚀 ✈ │
│ │
│ █████ terrain █████ │
│ │
│ ☁ CLOUD ☁ │
│ ← drawn last │
│ │
└─────────────────────────────────────────────┘

The rendering order is approximately:

1. Distant space background
2. Near background
3. Game objects
├── Player
├── Enemies
├── Bullets
├── Terrain / walls
├── Gears
└── Power-ups
4. Clouds
5. UI / score / other foreground information

The order is important because later objects are drawn on top of objects that were drawn earlier.

This is particularly important for the clouds. A cloud is not simply another obstacle occupying a position in the world. It is also a visual layer that can hide objects behind it.

For example:

Without cloud:

Enemy
Player
🚀


With cloud:

☁ ☁ ☁ ☁
☁ ✈ ☁
☁ ☁ ☁ ☁
Player
🚀

The enemy still exists and continues to behave normally, but the cloud is rendered afterwards and therefore hides it visually.

Parallax Effect

The two background layers move at different speeds.

For example:

Distant background:
←───────

Near background:
←──────────────

Game objects:
←────────────────────

The distant layer moves more slowly than the closer layer. Since the player moves through the level while these layers move at different rates, the result creates an impression of depth even though the game is fundamentally 2D.

This is a simple and effective technique called parallax scrolling.

The important thing is that the background layers are purely visual. They do not participate in the game's collision detection or gameplay logic.

The actual game world is therefore effectively separated into two concepts:

VISUAL WORLD
├── distant background
└── near background

GAME WORLD
├── player
├── enemies
├── bullets
├── terrain
└── power-ups

The backgrounds can move independently without affecting the position or behaviour of the actual game entities.

Why Rendering Order Matters

This also illustrates an important general principle of 2D rendering:

The order in which objects are drawn determines their visual depth.

If the cloud were drawn before the enemy, the enemy would simply appear on top of it:

Cloud
Enemy
Result: enemy visible

But when the enemy is drawn first:

Enemy
Cloud
Result: enemy hidden

No complicated 3D geometry is required. The visual effect is achieved simply by controlling the rendering order.

For Technozomians, this gives a relatively small collection of sprites a surprisingly rich visual hierarchy:

Far background
Near background
Game objects
Foreground / clouds

Combined with the different scrolling speeds of the background layers, this creates the illusion of a much more complex environment than the underlying 2D representation actually is.


Android-Specific Configuration

Although most of the game is written using platform-independent LibGDX code, the Android project contains a small amount of Android-specific code in AndroidLauncher.

One of the important parts is the handling of the Android system navigation controls.

The game uses a method such as:

private void hideVirtualButtons() {
getWindow().setFlags(
WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN
);

getWindow().getDecorView().setSystemUiVisibility(
View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
View.SYSTEM_UI_FLAG_FULLSCREEN |
View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
);
}

The exact Android flags depend on the Android version and implementation, but the purpose is straightforward:

The game should use the whole screen rather than displaying the standard Android navigation controls over the game.

This is particularly important for a game because the navigation bar would otherwise occupy part of the screen and could interfere with the game's visual layout and touch controls.

The application also needs to prevent the device from going to sleep while the game is being played.

Conceptually:

getWindow().addFlags(
WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
);

This tells Android:

Keep the display active while this activity is running.

Without this, a player could be in the middle of a level, stop touching the screen for a while, and have the device automatically dim or lock the display.


Platform-Independent Game, Platform-Specific Launcher

This illustrates an important architectural feature of LibGDX.

Most of the game can remain independent of Android:

Core
+---------+---------+
│ │ │
Game Entities Rendering
│ │ │
+---------+---------+
LibGDX

while the Android-specific part stays in:

Android
└── AndroidLauncher
├── Fullscreen
├── Navigation buttons
├── Keep screen awake
└── Android configuration

This separation means that the actual game logic does not need to contain Android-specific code everywhere.

The AndroidLauncher acts as the bridge between the Android operating system and the platform-independent LibGDX game.

That is one of the useful architectural ideas behind the LibGDX project structure:

Game code
(platform independent)
LibGDX
+----------+----------+
│ │
Android Desktop
│ │
AndroidLauncher Desktop launcher

The same core game can therefore be launched on different platforms while the launcher handles the things specific to that platform.


Final Thoughts

When Technozomians was developed in 2016, making a game required considerably more hands-on work than it often does today. I had to deal directly with things such as the game loop, sprite rendering, animation states, collision detection, screen coordinates, viewports, touch input, resource loading, sound, level representation and Android-specific behaviour.

Today, most of these problems are handled by the game engine.

Modern engines such as Unity, Unreal Engine and Godot provide ready-made systems for rendering, input, audio, animation, physics, cameras, asset management and deployment to different platforms. The developer can therefore spend much more time working on the actual game rather than building the infrastructure required to make a game run in the first place.

And that is a very good thing.

You don't need to implement your own collision detection system before you can make your first game. You don't need to write your own sprite renderer. You don't need to worry about every screen resolution or write your own audio playback system.

But I still think there is value in understanding what is happening underneath.

An engine can tell you:

"These two objects collided."

But it does not decide whether that collision means:

Bullet + Enemy
Enemy damaged
Enemy destroyed
Explosion
Score +100
Play explosion sound

That is game logic, and that part still belongs to the developer.

The same applies to almost everything else described in this article. An engine can provide a camera, but you decide how the camera should behave. It can provide animation, but you decide which animation should play and when. It can provide physics, but you decide what happens when an object hits another object.

You Don't Have to Build Everything Yourself

Looking at the old Technozomians source code today, some of the things I implemented manually would probably be considered unnecessary work in a modern game project.

The level data was encoded in strings.

The font was a bitmap and a character map.

Collision detection was performed directly between objects.

The game maintained its own entity properties and states.

The rendering system rebuilt the frame every cycle.

The camera and viewport had to be configured for different mobile screens.

Android-specific code had to deal with navigation buttons and keeping the screen awake.

Today, an engine can provide ready-made solutions for most of these tasks.

That does not make the old approach useless, though. On the contrary, writing these things manually forces you to understand why they exist in the first place.

And that is perhaps the most useful thing this old project can offer to someone who is thinking about making a game for the first time.

A Possible Starting Point

If you have never made a game before but are interested in trying, you don't need to start by writing a game engine.

Start with an engine.

Create something very small. Make an object move. Make it respond to input. Add another object. Make them collide. Add a sound. Add a score. Then add a little more.

At some point, you will probably encounter exactly the same concepts that appear in this project:

Input
Game Logic
Object State
Collision
Animation
Rendering
Sound / Feedback
Next Frame

The engine will hide much of the technical complexity, but understanding this basic flow makes it much easier to understand what the engine is actually doing for you.

And once you understand that, the engine stops looking like magic.

It becomes a set of tools.

From 1988 to Today

Perhaps that is the most interesting thing about this project for me.

In 1988, I was writing a 3D wireframe program in BASIC on a ZX Spectrum and waiting for the computer to finish drawing the image.

Years later, I used some of the same ideas to experiment with 3D graphics on Android.

Then came Technozomians, built with Java and LibGDX, followed by many other experiments and projects.

Today, a beginner can open a modern game engine, describe what they want to build, and increasingly even use AI to help create the initial game. Roblox, for example, has recently introduced AI-assisted tools that can turn a natural-language description into a playable starting point.

That is an enormous change from writing code onto a cassette tape.

But the fundamental ideas have not changed nearly as much as the tools have.

There is still a game loop.

There are still objects with positions and states.

Something still has to decide what happens when two objects collide.

The screen still has to be rendered frame by frame.

And, ultimately, somebody still has to decide what the game is supposed to do.

That is why I think an old project like Technozomians can still be useful. Not because this is how games should be made today — they shouldn't be.

But because it shows what is happening underneath the layers of technology that modern game engines normally hide.

And sometimes, understanding what is underneath is the easiest way to understand what is on top.