Asteroids

For the past few weeks I've been working on writing and testing an object oriented encapsulation of the free open source SDL and NanoVG libraries. Below is a short video clip of one of my demo program testing the graphics library. This demo recreates the classic arcade game Asteroids using Free Pascal. Aside from the coins required and high score table, I believe I somewhat accurately recreated the game.
This program has been tested and runs well on Windows, Linux, Mac, and Raspberry Pi

Video

Here is a video capture of my recreation of Asteroids using my soon to be released vector graphics library.
Video: Asteroids


Source Code

The following is a listing of the Pascal method responsible for ALL of the logic in my recreation of the Asteroids arcade game. You might find it interesting.
procedure TAsteroidsGame.Logic(Width, Height: Integer; const Time: Double);

  { Wrap things to the gamefield }
  procedure Wrap(var Pos: TPointF);
  begin
    if Pos.X < FGamefield.Left then
      Pos.X := FGamefield.Right
    else if Pos.X > FGamefield.Right then
      Pos.X := FGamefield.Left;
    if Pos.Y < FGamefield.Top then
      Pos.Y := FGamefield.Bottom
    else if Pos.Y > FGamefield.Bottom then
      Pos.Y := FGamefield.Top;
  end;

  { Add an explosion source }
  procedure Explode(Pos: TPointF);
  begin
    FExplode[FExplodeIndex].Pos := Pos;
    FExplode[FExplodeIndex].Seed := Round(Random * 9999) + 1;
    FExplode[FExplodeIndex].Time := Time;
    FExplodeIndex := (FExplodeIndex + 1) mod High(FExplode);
  end;

  { Spawn two rocks from one rock }
  procedure RockSplit(R: PRock);
  var
    S: Float;
    N: TRock;
    I: Integer;
  begin
    S := 1 / R.Size;
    R.Alive := False;
    Explode(R.Pos);
    for I := 0 to 1 do
    begin
      N.Angle := Random * 5;
      N.Size := R.Size;
      N.Shape := Trunc(Random * 3);
      N.Dir := (Random - 0.5) * 0.01 * S;
      N.Speed.X := (Random - 0.5) * S;
      N.Speed.Y := (Random - 0.5) * S;
      N.Pos.X := R.Pos.X - (I - 0.5) * 10;
      N.Pos.Y := R.Pos.Y - (I - 0.5) * 10;
      N.Alive := True;
      FNewRocks.Push(N);
    end;
  end;

  { Check if the player intersects a rock or saucer }
  function PlayerInShape(const Player, Shape: TShape): Boolean;
  var
    A, B: TLine;
    I, J: Integer;
  begin
    Result := False;
    for I := 0 to Player.Length do
    begin
      A.P0 := Player[I mod Player.Length];
      A.P1 := Player[(I + 1) mod Player.Length];
      for J := 0 to Shape.Length do
        begin
          B.P0 := Shape[J mod Shape.Length];
          B.P1 := Shape[(J + 1) mod Shape.Length];
          if A.Intersects(B) then
            Exit(True);
        end;
    end;
  end;

const
  NumLives = 3;
  TurnSpeed = 0.04;
  SpeedCap = 3;
  BulletLife = 2.5;
var
  P: TPointF;
  B: PBullet;
  R: PRock;
  I, J: Integer;
begin
  { Reset the rock field if it has been destroyed }
  for I := 0 to FRocks.Length - 1 do
    if FRocks[I].Alive then
    begin
      FRockGone := Time;
      Break;
    end;
  if Time - FRockGone > 2 then
    Reset;
  { Player logic }
  with FPlayer do
  begin
    { Start a new game if enter was pressed and player has no lives }
    if (Lives < 1) and (FHyperspace) then
    begin
      FHyperspace := False;
      Reset;
      Alive := True;
      Lives := NumLives;
      Exit;
    end;
    { Process input }
    if Alive then
    begin
      { Hyperspace }
      if FHyperspace then
      begin
        Speed := NewPointF(0, 0);
        Pos.X := FGamefield.Left + Random * (FGamefield.Width - 200) + 100;
        Pos.Y := FGamefield.Top + Random * (FGamefield.Height - 200) + 100;
      end;
      { Rotate the player }
      if FLeft xor FRight then
        if FLeft then
          Angle := Angle + TurnSpeed
        else
          Angle := Angle - TurnSpeed;
      { Add thrust }
      if FThrust then
      begin
        P := NewPointF(0, -0.01).Rotate(Angle);
        Speed := Speed + P;
      end;
      { Fire bullets }
      if FFire then
      begin
        for I := Low(Bullets) to High(Bullets) do
        begin
          B := @Bullets[I];
          if Time - B.Time > BulletLife then
            B.Alive := False;
          if B.Alive then
            Continue;
          B.Pos := NewPointF(0, -20).Rotate(Angle) + Pos;
          B.Speed := NewPointF(0, -4).Rotate(Angle) + Speed;
          B.Alive := True;
          B.Time := Time;
          Break;
        end;
      end;
    end;
    { Cap the maximum player speed }
    if Speed.Distance > SpeedCap then
    begin
      Speed.Normalize;
      Speed := Speed * SpeedCap;
    end;
    Pos := Pos + Speed * 2;
    { Wrap the player position }
    Wrap(Pos);
    { And generate geometry for hit testing }
    for I := 0 to Geometry.Length - 1 do
      Geometry[I] := FPlayerShape[I].Rotate(Angle) + Pos;
    FHyperspace := False;
    FFire := False;
    { Move bullets }
    for I := Low(Bullets) to High(Bullets) do
    begin
      B := @Bullets[I];
      if Time - B.Time > BulletLife then
        B.Alive := False;
      if not B.Alive then
        Continue;
      B.Pos := B.Pos + B.Speed;
      Wrap(B.Pos);
    end;
  end;
  { Rock logic }
  for I := 0 to FRocks.Length - 1 do
  begin
    R := @FRocks.Items[I];
    if not R.Alive then
      Continue;
    R.Angle := R.Angle + R.Dir;
    R.Pos := R.Pos + R.Speed;
    { Wrap the rock position around the gamefield }
    Wrap(R.Pos);
    R.Geometry.Length := FRockShapes[R.Shape].Length;
    { Generate rock geometry for hit testing }
    for J := 0 to R.Geometry.Length - 1 do
      R.Geometry[J] := FRockShapes[R.Shape][J].Rotate(R.Angle) * R.Size + R.Pos;
  end;
  { Saucer logic }
  with FSaucer do
  begin
    { If the player is alive and enough time has passed then ... }
    if FPlayer.Alive and (Time - FWaveTime > (15 + Random * 10) / (FWave * 0.5)) and (not Alive) then
    begin
      { Mark the time and generate the saucer data }
      FWaveTime := Time;
      Alive := True;
      { Is it a big or small saucer? More waves have a higher chance of a small saucer. }
      if Random * FWave < 1.5 then
        Size := 1
      else
        Size := 0.5;
      Alt := Random * 300 + 150;
      if Random < 0.5 then
        Dir := -1
      else
        Dir := 1;
      if Random < 0.5 then
        Flip := -1
      else
        Flip := 1;
      Dist := Random * 700 + 400;
      Pos.Y := FGamefield.Top + Alt;
      if Dir < 0 then
        Pos.X := FGamefield.Right + 50
      else
        Pos.X := FGamefield.Left - 50;
      { Don't let the saucer shoot as soon as it spawns }
      LastShot := Time + 2 + Random * 3 * Size;
    end;
    { Update the saucer position }
    if Alive then
    begin
      Pos.X := Pos.X + (1 / Size) * Dir * 0.75;
      if Dir < 0 then
      begin
        if (FGamefield.Right - 50) - Pos.X > Dist then
          if Flip > 0 then
            Pos.Y := Pos.Y + (1 / Size) * 0.75
          else
            Pos.Y := Pos.Y - (1 / Size) * 0.75;
        if Pos.X < FGamefield.Left - 50 then
          Alive := False;
      end
      else
      begin
        if Pos.X - (FGamefield.Left - 50) > Dist then
          if Flip > 0 then
            Pos.Y := Pos.Y + (1 / Size) * 0.75
          else
            Pos.Y := Pos.Y - (1 / Size) * 0.75;
        if Pos.X > FGamefield.Right + 50 then
          Alive := False
      end;
      if Pos.Y < FGamefield.Top - 50 then
        Alive := False
      else if Pos.Y > FGamefield.Bottom + 50 then
        Alive := False;
    end;
    { Generate the saucer geometry for hit testing }
    if Alive then
    begin
      Geometry.Length := FSaucerShape.Length;
      for I := 0 to Geometry.Length - 1 do
        Geometry[I] := FSaucerShape[I] * Size + Pos;
    end;
    { If both the saucer and player are alive, shoot bullets at the player occasionally }
    if Alive and (Time > LastShot) and FPlayer.Alive then
    begin
      { Add a bit of randomness to shooting intervals }
      LastShot := Time + 0.75 + Random * Size * 2;
      for J := Low(Bullets) to High(Bullets) do
      begin
        B := @Bullets[J];
        if B.Alive then
          Continue;
        B.Pos := FSaucer.Pos;
        P := FPlayer.Pos;
        { Saucer bullets aren't 100% accurate }
        P.X := P.X + (Random - 0.5) * 100;
        P.Y := P.Y + (Random - 0.5) * 100;
        B.Speed := P - Pos;
        B.Speed.Normalize;
        B.Pos := B.Pos + B.Speed * (FSaucerRadius / 3) * Size;
        B.Speed := B.Speed * (1 / Size) * 1.5;
        B.Alive := True;
        Break;
      end;
    end;
    { Move saucer bullets }
    for I := Low(Bullets) to High(Bullets) do
    begin
      B := @Bullets[I];
      if not B.Alive then
        Continue;
      B.Pos := B.Pos + B.Speed;
      { Saucer bullets expire if they move off the gamefield }
      B.Alive := FGamefield.Contains(B.Pos.X, B.Pos.Y);
    end;
  end;
  { When rocks are destroyed they spawn two smaller rocks. These smaller rocks
    are temporarily stored a new rock collection. }
  FNewRocks.Length := 0;
  { Detect rock collisions with player and bullets }
  for I := 0 to FRocks.Length - 1 do
  begin
    R := @FRocks.Items[I];
    if not R.Alive then
      Continue;
    { Test collisions with rocks and bullets }
    for J := Low(FPlayer.Bullets) to High(FPlayer.Bullets) do
    begin
      B := @FPlayer.Bullets[J];
      if not B.Alive then
        Continue;
      if (B.Pos.Distance(R.Pos) < FRockRadius * R.Size) and
        PointInShape(B.Pos.X, B.Pos.Y, R.Geometry) then
      begin
        B.Alive := False;
        { Scoring for destroying a rock with a bullet }
        if R.Size = 1 then
          AddScore(20)
        else if R.Size > 0.5 then
          AddScore(50)
        else
          AddScore(100);
        R.Size := R.Size - 0.34;
        R.Alive := False;
        if R.Size < 0 then
          Explode(R.Pos)
        else
          RockSplit(R);
        Break;
      end;
    end;
    { If the rock was destroyed continue }
    if not R.Alive then
      Continue;
    { Test collisions with rocks and the player }
    if FPlayer.Alive then
      if (FPlayer.Pos.Distance(R.Pos) < FRockRadius * R.Size + FPlayerRadius) and
        PlayerInShape(FPlayer.Geometry, R.Geometry) then
      begin
        { Scoring for destroying a rock with your ship }
        if R.Size = 1 then
          AddScore(20)
        else if R.Size > 0.5 then
          AddScore(50)
        else
          AddScore(100);
        R.Size := R.Size - 0.34;
        R.Alive := False;
        if R.Size < 0 then
        begin
          R.Alive := False;
          Explode(R.Pos);
        end
        else
          RockSplit(R);
        { Record the time of death, subtract a life and explode }
        FPlayer.Alive := False;
        FPlayer.Death := Time;
        FPlayer.Lives := FPlayer.Lives - 1;
        Explode(FPlayer.Pos);
        FFirstRun := False;
        Break;
      end;
  end;
  { Rock collisions are complete. Add the new rocks to the main rock collection. }
  FRocks.PushRange(FNewRocks.Items);
  { If the player has no lives then exit }
  if FPlayer.Lives < 1 then
    Exit;
  { Check for player saucer interaction }
  if FPlayer.Alive then
  begin
    { Check if player bullets hit the saucer }
    if FSaucer.Alive then
      for I := Low(FPlayer.Bullets) to High(FPlayer.Bullets) do
      begin
        B := @FPlayer.Bullets[I];
        if not B.Alive then
          Continue;
        if (B.Pos.Distance(FSaucer.Pos) < FSaucerRadius * FSaucer.Size) and
          PointInShape(B.Pos.X, B.Pos.Y, FSaucer.Geometry) then
        begin
          B.Alive := False;
          FSaucer.Alive := False;
          Explode(FSaucer.Pos);
          { Scoring for destroying a saucer with a bullet }
          if FSaucer.Size > 0.75 then
            AddScore(200)
          else
            AddScore(1000);
          Break;
        end;
      end;
    { Check if saucer bullets hit the player }
    for I := Low(FSaucer.Bullets) to High(FSaucer.Bullets) do
    begin
      B := @FSaucer.Bullets[I];
      if not B.Alive then
        Continue;
      if (B.Pos.Distance(FPlayer.Pos) < FPlayerRadius) and
        PointInShape(B.Pos.X, B.Pos.Y, FPlayer.Geometry) then
      begin
        { The player was hit by a saucer bullet }
        B.Alive := False;
        FPlayer.Alive := False;
        FPlayer.Death := Time;
        FPlayer.Lives := FPlayer.Lives - 1;
        Explode(FPlayer.Pos);
        FFirstRun := False;
        Break;
      end;
    end;
  end;
  { Again exit if the player is out of lives }
  if FPlayer.Lives < 1 then
    Exit;
  { Check if the player ran into the saucer }
  if FPlayer.Alive and FSaucer.Alive then
    if (FPlayer.Pos.Distance(FSaucer.Pos) < FSaucerRadius * FSaucer.Size + FPlayerRadius) and
      PlayerInShape(FPlayer.Geometry, FSaucer.Geometry) then
    begin
      { Record the time of death, subtract a life and explode both the saucer and your ship }
      FSaucer.Alive := False;
      FPlayer.Alive := False;
      FPlayer.Death := Time;
      FPlayer.Lives := FPlayer.Lives - 1;
      Explode(FSaucer.Pos);
      Explode(FPlayer.Pos);
      FFirstRun := False;
      { Scoring for destroying a saucer with your ship }
      if FSaucer.Size > 0.75 then
        AddScore(200)
      else
        AddScore(1000);
    end;
  { Respawn the player in a safe space }
  if (not FPlayer.Alive) and (Time - FPlayer.Death > 4) then
  begin
    FPlayer.Angle := 0;
    FPlayer.Pos := FGamefield.MidPoint;
    FPlayer.Speed := NewPointF(0, 0);
    for I := 0 to FRocks.Length - 1 do
    begin
      R := @FRocks.Items[I];
      if not R.Alive then
        Continue;
      if R.Pos.Distance(FPlayer.Pos) < 150 then
        Exit;
    end;
    FPlayer.Alive := True;
  end;
end;