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;