Eaglev0.1.0

Asteroids

Vector ship, splitting rocks, wrap-around space and thruster particles.

A/D rotate · W thrust · Space fire

Source: examples/asteroids/main.cr

examples/asteroids/main.cr

require "../../src/eagle"
include Eagle

# Asteroids: rotate with A/D or left stick, thrust with W, fire with space / A button.
class Ship < Node2D
  property velocity = Vec2::ZERO
  getter? alive = true
  @respawn = 0_f32
  @invuln = 2_f32

  def process(dt : Float32)
    unless @alive
      @respawn -= dt
      if @respawn <= 0
        @alive = true; @invuln = 2_f32
        self.position = v2(Window.width / 2, Window.height / 2)
        @velocity = Vec2::ZERO
      end
      return
    end
    @invuln -= dt
    self.rotation += Input.axis("left", "right") * 4 * dt
    if Input.down?("thrust")
      @velocity += Vec2.from_angle(rotation) * 380 * dt
      thrust.emitting = true
    else
      thrust.emitting = false
    end
    @velocity *= (1 / (1 + dt * 0.4))
    self.position += @velocity * dt
    self.position = v2(position.x % Window.width, position.y % Window.height)
    thrust.direction = rotation + Math::PI
  end

  def thrust : Particles2D
    (child?("thrust") || add(Particles2D.new("thrust", amount: 120, position: Vec2::ZERO).tap { |p|
      p.world_space = true; p.lifetime = 0.35; p.speed = 80..160; p.spread = 0.3
      p.scale_start = 0.4; p.scale_end = 0; p.color_over_life = [Color::ORANGE, Color::RED.with_alpha(0)]
      p.blend = GPU::BlendMode::Additive; p.emitting = false
    })).as(Particles2D)
  end

  def invulnerable? : Bool; @invuln > 0; end

  def die
    @alive = false
    @respawn = 2_f32
    thrust.emitting = false
  end

  def draw(g : Graphics)
    return unless @alive
    return if invulnerable? && (Clock.elapsed * 10).to_i.even?
    g.polygon([v2(16, 0), v2(-12, 10), v2(-6, 0), v2(-12, -10)], DrawMode::Line, Color::WHITE)
  end
end

class Rock < Node2D
  property velocity : Vec2
  property size : Int32
  getter radius : Float32
  @points : Array(Vec2)
  @spin : Float32

  def initialize(position : Vec2, @size : Int32, rng : Random)
    super("rock", position)
    @radius = (@size * 14).to_f32
    @velocity = Vec2.from_angle(rng.rand * Math::PI * 2, 40 + rng.rand * 60 + (3 - @size) * 30)
    @spin = (rng.rand - 0.5).to_f32 * 2
    n = 10
    @points = Array(Vec2).new(n) { |i| Vec2.from_angle(Math::PI * 2 * i / n, @radius * (0.7 + rng.rand * 0.4)) }
  end

  def process(dt : Float32)
    self.position += @velocity * dt
    self.rotation += @spin * dt
    self.position = v2(position.x % Window.width, position.y % Window.height)
  end

  def draw(g : Graphics)
    g.polygon(@points, DrawMode::Line, Color.gray(0.8))
  end
end

class Bullet < Node2D
  property velocity : Vec2
  @life = 1.1_f32
  def initialize(position : Vec2, @velocity : Vec2)
    super("bullet", position)
  end
  def process(dt : Float32)
    self.position += @velocity * dt
    self.position = v2(position.x % Window.width, position.y % Window.height)
    @life -= dt
    queue_free if @life <= 0
  end
  def draw(g : Graphics)
    g.circle(0, 0, 2, color: Color::YELLOW)
  end
end

class AsteroidsGame < App
  @ship = Ship.new("ship", v2(400, 300))
  @score = 0
  @lives = 3
  @wave = 0
  @rng = Random.new(7)
  @fx = Particles2D.new(amount: 0)
  @snd_shoot : Sound? = nil
  @snd_boom : Sound? = nil
  @cooldown = 0_f32
  @over = false

  def load
    Input.map "left", Key::A, Key::Left, Input.axis(GamepadAxis::LeftX, -1)
    Input.map "right", Key::D, Key::Right, Input.axis(GamepadAxis::LeftX, 1)
    Input.map "thrust", Key::W, Key::Up, GamepadButton::B, Input.axis(GamepadAxis::TriggerRight, 1)
    Input.map "fire", Key::Space, GamepadButton::A
    @snd_shoot = Sound.tone(880, 0.05, Sound::Wave::Square, 0.15)
    @snd_boom = Sound.tone(90, 0.3, Sound::Wave::Noise, 0.35)
    @fx.lifetime = 0.6; @fx.speed = 40..180; @fx.spread = Math::PI; @fx.scale_start = 0.3; @fx.scale_end = 0
    @fx.color_over_life = [Color::WHITE, Color.gray(0.5).with_alpha(0)]
    SceneTree.root.add(@ship, @fx)
    spawn_wave
  end

  def spawn_wave
    @wave += 1
    (3 + @wave).times do
      pos = v2(@rng.rand * Window.width, @rng.rand * Window.height)
      pos = v2(pos.x + 200, pos.y) if pos.distance(@ship.position) < 150
      SceneTree.root.add(Rock.new(pos, 3, @rng))
    end
  end

  def update(dt : Float32)
    @cooldown -= dt
    if @ship.alive? && Input.down?("fire") && @cooldown <= 0 && !@over
      @cooldown = 0.18_f32
      SceneTree.root.add(Bullet.new(@ship.position + Vec2.from_angle(@ship.rotation, 16), @ship.velocity + Vec2.from_angle(@ship.rotation, 500)))
      @snd_shoot.try(&.play(pitch: 0.9 + rand * 0.2))
    end
    rocks = SceneTree.root.children_of(Rock)
    bullets = SceneTree.root.children_of(Bullet)
    rocks.each do |r|
      bullets.each do |b|
        next if b.queued_free?
        if b.position.distance(r.position) < r.radius
          b.queue_free
          split(r)
          break
        end
      end
      if @ship.alive? && !@ship.invulnerable? && !r.queued_free? && r.position.distance(@ship.position) < r.radius + 10
        @lives -= 1
        explode(@ship.position, 40)
        @ship.die
        @over = true if @lives <= 0
      end
    end
    spawn_wave if rocks.empty?
    if @over && Input.pressed?(Key::R)
      SceneTree.root.children_of(Rock).each(&.queue_free)
      @score = 0; @lives = 3; @wave = 0; @over = false
      SceneTree.flush_deferred
      spawn_wave
    end
    Eagle.quit if Input.pressed?(Key::Escape)
  end

  def split(r : Rock)
    r.queue_free
    @score += {3 => 20, 2 => 50, 1 => 100}[r.size]
    explode(r.position, 12 * r.size)
    @snd_boom.try(&.play(pitch: 0.7 + r.size * 0.2))
    if r.size > 1
      2.times { SceneTree.root.add(Rock.new(r.position, r.size - 1, @rng)) }
    end
  end

  def explode(at : Vec2, n : Int32)
    @fx.position = at
    @fx.emit(n)
  end

  def draw(g : Graphics)
    g.print("score #{@score}   lives #{@lives}   wave #{@wave}", 12, 10, Color::WHITE)
    g.print("GAME OVER. R to restart", Window.width / 2, Window.height / 2, Color::RED, align: TextAlign::Center) if @over
  end
end

Eagle.run(AsteroidsGame, title: "Eagle Asteroids", width: 800, height: 600)