Download Now
Forums

    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;