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;