Skip to main content
This example demonstrates how to build a complete asteroid-style survival game in Talon. You’ll learn about object-oriented game design, physics simulation, collision detection, and game state management.

Overview

The asteroid game includes:
  • Player spaceship with rotation and acceleration
  • Meteor generation and movement
  • Collision detection between player and meteors
  • Game over and restart functionality
  • Score tracking based on survival time
  • Pause functionality

Game Preview

  • Controls: Arrow keys to move (UP for thrust, LEFT/RIGHT for rotation)
  • Objective: Survive as long as possible while avoiding meteors
  • Features: Screen wrapping, collision detection, pause with ‘P’ key

Complete Code

import "raylib" for Color, Raylib, Rectangle, Vector2, Camera2D, KeyCode, Texture2D, Vector3
import "math" for Math

var PLAYER_BASE_SIZE = 20
var PLAYER_SPEED = 6.0
var PLAYER_MAX_SHOOTS = 10 
var DEG2RAD = 0.017453

var METEORS_SPEED = 2 
var MAX_MEDIUM_METEORS = 8 
var MAX_SMALL_METEORS = 16

var ScreenWidth = 800
var ScreenHeight = 450

class Meteor {
  construct new(position, speed, radius, active, color) {
    _position = position 
    _speed = speed 
    _radius = radius
    _active = active 
    _color = color
  }
  
  position { _position }
  speed { _speed }
  active { _active }
  color { _color }
  radius { _radius }

  position=(value) { _position = value }
  speed=(value) { _speed = value }
  active=(value) { _active = value }
  color=(value) { _color = value }
  radius=(value) { _radius = value }
}

class Player {
  construct new(position, speed, acceleration, rotation, collider, color) {
    _position = position
    _speed = speed 
    _acceleration = acceleration 
    _rotation = rotation 
    _collider = collider
    _color = color
  }
  position { _position }
  speed { _speed }
  acceleration { _acceleration }
  rotation { _rotation }
  collider { _collider }
  color { _color }

  position=(value) { _position = value }
  speed=(value) { _speed = value }
  acceleration=(value) { _acceleration = value }
  rotation=(value) { _rotation = value }
  collider=(value) { _collider = value }
  color=(value) { _color = value }
}

class Game {
  initGame() {
    var posx = 0
    var posy = 0
    var velx = 0
    var vely = 0
    var correctRange = false

    _pause = false
    _gameOver = false
    _framesCounter = 0
    _shipHeight = (PLAYER_BASE_SIZE / 2) / Math.tan(20 * DEG2RAD)

    _mediumMeteor = []
    _smallMeteor = []

    var x = ScreenWidth / 2
    var y = ScreenHeight / 2 - _shipHeight / 2

    _player = Player.new(
       Vector2.new(x, y),
       Vector2.new(0, 0), 
       0, 
       0, 
       Vector3.new(
        x + Math.sin(0 * DEG2RAD) * (_shipHeight / 2.5),
        y - Math.cos(0 * DEG2RAD) * (_shipHeight / 2.5),
        12),
      Color.LightGray
    )

    // Initialize medium meteors
    for (i in 0...MAX_MEDIUM_METEORS) {
      posx = Raylib.getRandomValue(0, ScreenWidth)

      while (!correctRange) {
        if (posx > ScreenWidth/2 - 150 && posx < ScreenWidth/2 + 150) {
          posx = Raylib.getRandomValue(0, ScreenWidth)
        } else { 
          correctRange = true
        }
      }

      correctRange = false
      posy = Raylib.getRandomValue(0, ScreenHeight)

      while (!correctRange) {
        if (posy > ScreenHeight/2 - 150 && posy < ScreenHeight/2 + 150) {
          posy = Raylib.getRandomValue(0, ScreenHeight)
        } else {
          correctRange = true
        }
      }

      correctRange = false
      velx = Raylib.getRandomValue(-METEORS_SPEED, METEORS_SPEED)
      vely = Raylib.getRandomValue(-METEORS_SPEED, METEORS_SPEED)

      while (!correctRange) {
        if (velx == 0 && vely == 0) {
          velx = Raylib.getRandomValue(-METEORS_SPEED, METEORS_SPEED)
          vely = Raylib.getRandomValue(-METEORS_SPEED, METEORS_SPEED)
        } else {
          correctRange = true
        }
      }
      _mediumMeteor.add(
         Meteor.new(Vector2.new(posx, posy), Vector2.new(velx, vely), 20, true, Color.Green)
      )
    }

    // Initialize small meteors (similar logic)
    for (j in 0...MAX_SMALL_METEORS) {
      // ... (similar initialization)
      _smallMeteor.add(
         Meteor.new(Vector2.new(posx, posy), Vector2.new(velx, vely), 20, true, Color.Yellow)
      )
    }
  }

  construct new() {
    initGame()
  }

  player { _player }
  mediumMeteor { _mediumMeteor }
  smallMeteor { _smallMeteor }

  update() {
    if (!_gameOver) {
      if (Raylib.isKeyDown(KeyCode.KEY_P)) {
        _pause = !_pause
      }

      if (!_pause) {
        _framesCounter = _framesCounter + 1

        // Rotation
        if (Raylib.isKeyDown(KeyCode.KEY_LEFT)) {
          _player.rotation = player.rotation - 5
        }
        if (Raylib.isKeyDown(KeyCode.KEY_RIGHT)) {
          _player.rotation = _player.rotation + 5
        }

        // Speed calculation based on rotation
        _player.speed.x = Math.sin(_player.rotation*DEG2RAD)*PLAYER_SPEED
        _player.speed.y = Math.cos(_player.rotation*DEG2RAD)*PLAYER_SPEED

        // Acceleration control
        if (Raylib.isKeyDown(KeyCode.KEY_UP)) {
          if (_player.acceleration < 1) _player.acceleration = _player.acceleration + 0.04
        } else {
          if (_player.acceleration > 0) { 
            _player.acceleration = _player.acceleration - 0.02
          } else if (player.acceleration < 0) {
            _player.acceleration = 0 
          }
        }

        // Movement
        _player.position.x = _player.position.x + (_player.speed.x*_player.acceleration)
        _player.position.y = _player.position.y - (player.speed.y * player.acceleration)

        // Wall wrapping for player
        if (_player.position.x > ScreenWidth + _shipHeight) {
          _player.position.x = -(_shipHeight)
        } else if (_player.position.x < -(_shipHeight)) {
          player.position.x = ScreenWidth + _shipHeight
        }
        if (player.position.y > (ScreenHeight + _shipHeight)) {
          player.position.y = -(_shipHeight)
        } else if (_player.position.y < -(_shipHeight)) {
          _player.position.y = ScreenHeight + _shipHeight
        }

        // Update collider
        _player.collider = Vector3.new(
          player.position.x + Math.sin(player.rotation*DEG2RAD)*(_shipHeight/2.5),
          player.position.y - Math.cos(player.rotation*DEG2RAD)*(_shipHeight/2.5),
          12
        )

        // Check collisions with meteors
        for (i in 0...MAX_MEDIUM_METEORS) {
          if (Raylib.checkCollisionCircles(
            Vector2.new(player.collider.x, player.collider.y),
            player.collider.z,
            _mediumMeteor[i].position,
            _mediumMeteor[i].radius
          ) && _mediumMeteor[i].active) {
            _gameOver = true
          }
        }

        // Update meteor positions
        for (i in 0...MAX_MEDIUM_METEORS) {
          if (mediumMeteor[i].active) {
            mediumMeteor[i].position.x = mediumMeteor[i].position.x + mediumMeteor[i].speed.x
            mediumMeteor[i].position.y = mediumMeteor[i].position.y + mediumMeteor[i].speed.y

            // Wall wrapping for meteors
            if (mediumMeteor[i].position.x > ScreenWidth + mediumMeteor[i].radius) {
              mediumMeteor[i].position.x = -(mediumMeteor[i].radius)
            } else if (mediumMeteor[i].position.x < 0 - mediumMeteor[i].radius) {
              mediumMeteor[i].position.x = ScreenWidth + mediumMeteor[i].radius
            }
          }
        }
      }
    } else {
      if (Raylib.isKeyDown(KeyCode.KEY_ENTER)) {
        _gameOver = false
        initGame()
      }
    }
  }

  draw() {
    Raylib.beginDrawing()
    Raylib.clearBackground(Color.RayWhite)

    if (!_gameOver) {
      // Draw spaceship as triangle
      var v1 = Vector2.new(
        player.position.x + Math.sin(player.rotation*DEG2RAD)*(_shipHeight),
        player.position.y - Math.cos(player.rotation*DEG2RAD)*(_shipHeight)
      )
      var v2 = Vector2.new(
        player.position.x - Math.cos(player.rotation * DEG2RAD) * (PLAYER_BASE_SIZE / 2),
        player.position.y - Math.sin(player.rotation * DEG2RAD) * (PLAYER_BASE_SIZE / 2)
      )
      var v3 = Vector2.new(
        player.position.x + Math.cos(player.rotation * DEG2RAD) * (PLAYER_BASE_SIZE / 2),
        player.position.y + Math.sin(player.rotation * DEG2RAD) * (PLAYER_BASE_SIZE / 2)
      )

      Raylib.drawTriangle(v1, v2, v3, Color.Maroon)

      // Draw meteors
      for (i in 0...MAX_MEDIUM_METEORS) {
        if (mediumMeteor[i].active) {
          Raylib.drawCircleV(mediumMeteor[i].position, mediumMeteor[i].radius, Color.Gray)
        }
      }

      for (i in 0...MAX_SMALL_METEORS) {
        if (smallMeteor[i].active) {
          Raylib.drawCircleV(smallMeteor[i].position, smallMeteor[i].radius, Color.DarkGray)
        }
      }

      Raylib.drawText("TIME: %(_framesCounter / 60)", 10, 10, 20, Color.Black)

      if (_pause) Raylib.drawText("GAME PAUSED", ScreenWidth/2 - 40, ScreenHeight/2 - 40, 40, Color.Gray)
    } else {
      Raylib.drawText("PRESS [ENTER] TO PLAY AGAIN", 20, ScreenHeight/2 - 50, 20, Color.Gray)
    }

    Raylib.endDrawing()
  }
}

Raylib.initWindow(ScreenWidth, ScreenHeight, "classic game: asteroids survival")

var game = Game.new()

Raylib.setTargetFPS(60)

while (!Raylib.windowShouldClose()) {
  game.update()
  game.draw()
}

Raylib.closeWindow()

Code Breakdown

1. Game Entities

Meteor Class

class Meteor {
  construct new(position, speed, radius, active, color) {
    _position = position 
    _speed = speed 
    _radius = radius
    _active = active 
    _color = color
  }
}
The Meteor class encapsulates all properties needed for a meteor object, including position, velocity, and collision radius.

Player Class

class Player {
  construct new(position, speed, acceleration, rotation, collider, color) {
    // Properties for movement, rotation, and collision
  }
}
The Player class manages the spaceship’s state, including its position, rotation angle, acceleration, and collision boundaries.

2. Game Initialization

initGame() {
  _player = Player.new(
    Vector2.new(ScreenWidth / 2, ScreenHeight / 2),
    Vector2.new(0, 0),
    0, 0,
    Vector3.new(x, y, 12),
    Color.LightGray
  )
}
Initializes the player at the center of the screen with zero velocity. Meteors are spawned at random positions, ensuring they don’t spawn too close to the player.

3. Physics and Movement

// Calculate directional speed based on rotation
_player.speed.x = Math.sin(_player.rotation * DEG2RAD) * PLAYER_SPEED
_player.speed.y = Math.cos(_player.rotation * DEG2RAD) * PLAYER_SPEED

// Apply acceleration to position
_player.position.x = _player.position.x + (_player.speed.x * _player.acceleration)
_player.position.y = _player.position.y - (_player.speed.y * _player.acceleration)
The player’s movement uses trigonometry to convert rotation angle into directional velocity. Acceleration gradually increases/decreases for smooth control.

4. Screen Wrapping

if (_player.position.x > ScreenWidth + _shipHeight) {
  _player.position.x = -(_shipHeight)
} else if (_player.position.x < -(_shipHeight)) {
  _player.position.x = ScreenWidth + _shipHeight
}
When entities move off one edge of the screen, they reappear on the opposite edge, creating a seamless wraparound effect.

5. Collision Detection

if (Raylib.checkCollisionCircles(
  Vector2.new(player.collider.x, player.collider.y),
  player.collider.z,
  _mediumMeteor[i].position,
  _mediumMeteor[i].radius
) && _mediumMeteor[i].active) {
  _gameOver = true
}
Uses circle-circle collision detection between the player’s collider and each active meteor.

6. Rendering the Spaceship

var v1 = Vector2.new(
  player.position.x + Math.sin(player.rotation*DEG2RAD)*(_shipHeight),
  player.position.y - Math.cos(player.rotation*DEG2RAD)*(_shipHeight)
)
Raylib.drawTriangle(v1, v2, v3, Color.Maroon)
The spaceship is drawn as a triangle with vertices calculated based on the current rotation angle.

Key Concepts

  • Object-Oriented Design: Separate classes for different game entities
  • Vector Mathematics: Using Vector2 and Vector3 for positions and velocities
  • Trigonometry: Converting rotation angles to directional movement
  • Collision Detection: Circle-based collision using Raylib’s built-in functions
  • Game State Management: Handling play, pause, and game over states
  • Screen Wrapping: Creating a toroidal playing field

Enhancements to Try

  1. Add shooting: Implement bullets that destroy meteors
  2. Meteor breaking: Large meteors split into smaller ones when hit
  3. Power-ups: Add collectible items for shields or weapons
  4. Sound effects: Add audio for collisions and thrusters
  5. High score: Persist and display the best survival time

Build docs developers (and LLMs) love