program SpaceShooter;
const
SDL_WINDOW_SHOWN = 4;
SDL_RENDERER_ACCELLERATED = 2;
SDL_QUIT = 256;
SDL_KEYDOWN = 768;
SDL_KEYUP = 769;
SDLK_ESCAPE = 27;
SDLK_RETURN = 13;
SDLK_SPACE = 32;
WIDTH = 1440;
HEIGHT = 1080;
SCALE_W = 1440;
SCALE_H = 1080;
EXIT_FAILURE = 1;
fontfile = 'data/font.ttf';
logofile = 'data/logo.bmp';
spacelogofile = 'data/spacelogo.bmp';
shipfile = 'data/ship.bmp';
asteroidfile = 'data/asteroid.bmp';
saucerfile = 'data/saucer.bmp';
particlefile = 'data/particle.bmp';
SDL_SCANCODE_LEFT = 80;
SDL_SCANCODE_RIGHT = 79;
SDL_SCANCODE_UP = 82;
SDL_SCANCODE_DOWN = 81;
SDL_SCANCODE_SPACE = 44;
PI = 3.14159265358979323846;
MAX_PROJECTILES = 30;
MAX_ENEMIES = 20;
MAX_STARS = 100;
MAX_EXPLOSIONS = 15;
MAX_CIRCULAR = 8;
EXPTYPE_RED = 0;
EXPTYPE_BLUE = 1;
{ game states }
STATE_INTRO = 0;
STATE_PLAYING = 1;
STATE_DIED = 2;
STATE_GAMEOVER = 3;
type
Star = record
x, y, speed: integer;
end;
Projectile = record
x, y, w, h: integer;
active: boolean;
end;
Enemy = record
x, y, w, h: integer;
active: boolean;
end;
CircularEnemy = record
x, y: integer;
angle: real;
direction: integer;
active: boolean;
end;
Explosion = record
x, y: integer;
duration: integer;
cr, cg, cb: integer;
die: boolean;
active: boolean;
end;
var
running: boolean;
window_id, renderer, target_id: integer;
font_id: integer;
logo_tex, spacelogo_tex: integer;
ship_tex, asteroid_tex, saucer_tex, particle_tex: integer;
event_type_result: integer;
key: integer;
game_state: integer;
{ player }
player_x, player_y, player_w, player_h: integer;
player_die: boolean;
{ game data }
lives, score: integer;
regular_enemies_destroyed: integer;
last_shot_time: integer;
last_spawn_time: integer;
{ intro }
intro_alpha: integer;
intro_fading: boolean;
intro_timer: integer;
{ arrays - use active flag, no shifting }
stars: array[0..MAX_STARS - 1] of Star;
projectiles: array[0..MAX_PROJECTILES - 1] of Projectile;
enemies: array[0..MAX_ENEMIES - 1] of Enemy;
circular: array[0..MAX_CIRCULAR - 1] of CircularEnemy;
circular_cx: array[0..MAX_CIRCULAR - 1] of integer;
explosions: array[0..MAX_EXPLOSIONS - 1] of Explosion;
{ ---- collision detection ---- }
function aabb(ax, ay, aw, ah, bx, by, bw, bh: integer): boolean;
begin
aabb := not ((ax + aw <= bx) or (bx + bw <= ax) or (ay + ah <= by) or (by + bh <= ay));
end;
{ ---- explosion helpers ---- }
procedure add_explosion(ex, ey, etype: integer; d: boolean);
var
i: integer;
begin
i := 0;
while i < MAX_EXPLOSIONS do
begin
if explosions[i].active = false then
begin
explosions[i].x := ex;
explosions[i].y := ey;
explosions[i].duration := 60;
explosions[i].die := d;
explosions[i].active := true;
if etype = EXPTYPE_RED then
begin
explosions[i].cr := 255;
explosions[i].cg := 0;
explosions[i].cb := 0;
end
else
begin
explosions[i].cr := 0;
explosions[i].cg := 0;
explosions[i].cb := 255;
end;
exit;
end;
i := i + 1;
end;
end;
procedure subtract_life;
begin
lives := lives - 1;
if lives <= 0 then
game_state := STATE_GAMEOVER
else
game_state := STATE_DIED;
player_die := false;
end;
procedure update_explosions;
var
i: integer;
begin
for i := 0 to MAX_EXPLOSIONS - 1 do
begin
if explosions[i].active then
begin
explosions[i].duration := explosions[i].duration - 1;
if explosions[i].duration <= 0 then
begin
explosions[i].active := false;
if explosions[i].die then
subtract_life;
end;
end;
end;
end;
procedure draw_explosions;
var
i, k, dx, dy: integer;
begin
for i := 0 to MAX_EXPLOSIONS - 1 do
begin
if explosions[i].active then
begin
if explosions[i].die then
begin
{ large player death explosion }
for k := 0 to 39 do
begin
dx := (rand() mod 160) - 80;
dy := (rand() mod 160) - 80;
sdl_set_draw_color(renderer, 255, 200 + (rand() mod 56), rand() mod 80, 255);
sdl_draw_line(renderer, explosions[i].x, explosions[i].y,
explosions[i].x + dx, explosions[i].y + dy);
end;
{ white-hot core }
sdl_set_draw_color(renderer, 255, 255, 200, 255);
for k := 0 to 14 do
begin
dx := (rand() mod 40) - 20;
dy := (rand() mod 40) - 20;
sdl_draw_line(renderer, explosions[i].x + dx, explosions[i].y + dy,
explosions[i].x + (rand() mod 20) - 10, explosions[i].y + (rand() mod 20) - 10);
end;
end
else
begin
{ normal enemy explosion }
sdl_set_draw_color(renderer, explosions[i].cr, explosions[i].cg, explosions[i].cb, 255);
for k := 0 to 19 do
begin
dx := (rand() mod 60) - 30;
dy := (rand() mod 60) - 30;
sdl_draw_line(renderer, explosions[i].x, explosions[i].y,
explosions[i].x + dx, explosions[i].y + dy);
end;
end;
end;
end;
end;
{ ---- drawing helpers ---- }
procedure draw_gradient_triangle(x1, y1, y3: integer);
var
i, r, g, height: integer;
begin
if player_die then exit;
height := y3 - y1;
if height <= 0 then exit;
for i := 0 to height - 1 do
begin
r := 255 - (255 * i div height);
g := 255 * i div height;
sdl_set_draw_color(renderer, r, g, 255, 255);
sdl_draw_line(renderer, x1 - i, y1 + i, x1 + i, y1 + i);
end;
end;
procedure draw_gradient_diamond(cx, cy, hw, hh: integer);
var
i, r, b, g2: integer;
begin
for i := 0 to hh - 1 do
begin
r := 255 - (255 * i div hh);
b := 255 * i div hh;
sdl_set_draw_color(renderer, r, 255, b, 255);
sdl_draw_line(renderer, cx - i, cy - i, cx + i, cy - i);
end;
for i := 0 to hh - 1 do
begin
g2 := 255 - (255 * i div hh);
b := 255 * i div hh;
sdl_set_draw_color(renderer, 255, g2, b, 255);
sdl_draw_line(renderer, cx - i, cy + i, cx + i, cy + i);
end;
end;
procedure draw_gradient_circle(cx, cy, radius: integer);
var
i, w, color: integer;
begin
for i := 0 to radius - 1 do
begin
w := radius - i;
color := 255 * w div radius;
sdl_set_draw_color(renderer, color, 0, color, 255);
sdl_draw_line(renderer, cx - w, cy - i, cx + w, cy - i);
sdl_draw_line(renderer, cx - w, cy + i, cx + w, cy + i);
end;
end;
{ ---- find free slot helpers ---- }
function find_free_projectile: integer;
var
i: integer;
begin
find_free_projectile := -1;
for i := 0 to MAX_PROJECTILES - 1 do
if projectiles[i].active = false then
begin
find_free_projectile := i;
exit;
end;
end;
function find_free_enemy: integer;
var
i: integer;
begin
find_free_enemy := -1;
for i := 0 to MAX_ENEMIES - 1 do
if enemies[i].active = false then
begin
find_free_enemy := i;
exit;
end;
end;
function find_free_circular: integer;
var
i: integer;
begin
find_free_circular := -1;
for i := 0 to MAX_CIRCULAR - 1 do
if circular[i].active = false then
begin
find_free_circular := i;
exit;
end;
end;
{ ---- init / reset ---- }
procedure init_stars;
var
i: integer;
begin
for i := 0 to MAX_STARS - 1 do
begin
stars[i].x := rand() mod SCALE_W;
stars[i].y := rand() mod SCALE_H;
stars[i].speed := 1 + (rand() mod 3);
end;
end;
procedure clear_all_entities;
var
i: integer;
begin
for i := 0 to MAX_PROJECTILES - 1 do
projectiles[i].active := false;
for i := 0 to MAX_ENEMIES - 1 do
enemies[i].active := false;
for i := 0 to MAX_CIRCULAR - 1 do
circular[i].active := false;
for i := 0 to MAX_EXPLOSIONS - 1 do
explosions[i].active := false;
end;
procedure reset_game;
begin
player_x := (SCALE_W div 2) - 24;
player_y := SCALE_H - 80;
player_w := 48;
player_h := 48;
player_die := false;
clear_all_entities;
lives := 5;
score := 0;
regular_enemies_destroyed := 0;
last_shot_time := 0;
last_spawn_time := sdl_get_ticks();
init_stars;
game_state := STATE_PLAYING;
end;
procedure reset_round;
begin
clear_all_entities;
player_die := false;
player_x := (SCALE_W div 2) - 24;
player_y := SCALE_H - 80;
game_state := STATE_PLAYING;
end;
{ ---- game update ---- }
procedure update_game;
var
dx, dy: integer;
ticks: integer;
i, ei, ci: integer;
slot: integer;
crect_x, crect_y: integer;
new_cx: real;
begin
if (game_state <> STATE_PLAYING) then exit;
update_explosions;
{ player movement via keyboard state }
dx := 0;
dy := 0;
if sdl_is_key_pressed(SDL_SCANCODE_LEFT) <> 0 then dx := dx - 5;
if sdl_is_key_pressed(SDL_SCANCODE_RIGHT) <> 0 then dx := dx + 5;
if sdl_is_key_pressed(SDL_SCANCODE_UP) <> 0 then dy := dy - 5;
if sdl_is_key_pressed(SDL_SCANCODE_DOWN) <> 0 then dy := dy + 5;
player_x := player_x + dx;
player_y := player_y + dy;
if player_x < 0 then player_x := 0;
if player_x > SCALE_W - player_w then player_x := SCALE_W - player_w;
if player_y < 0 then player_y := 0;
if player_y > SCALE_H - player_h then player_y := SCALE_H - player_h;
{ shooting }
ticks := sdl_get_ticks();
if (sdl_is_key_pressed(SDL_SCANCODE_SPACE) <> 0) and (ticks - last_shot_time > 300) then
begin
slot := find_free_projectile();
if slot <> -1 then
begin
projectiles[slot].x := player_x + (player_w div 2) - 8;
projectiles[slot].y := player_y - 30;
projectiles[slot].w := 16;
projectiles[slot].h := 24;
projectiles[slot].active := true;
last_shot_time := ticks;
end;
end;
{ update projectiles and check collisions }
for i := 0 to MAX_PROJECTILES - 1 do
begin
if projectiles[i].active then
begin
projectiles[i].y := projectiles[i].y - 10;
{ off screen }
if projectiles[i].y < -24 then
begin
projectiles[i].active := false;
end
else
begin
{ check against regular enemies }
for ei := 0 to MAX_ENEMIES - 1 do
begin
if enemies[ei].active and projectiles[i].active then
begin
if aabb(projectiles[i].x, projectiles[i].y, projectiles[i].w, projectiles[i].h,
enemies[ei].x, enemies[ei].y, enemies[ei].w, enemies[ei].h) then
begin
add_explosion(enemies[ei].x + enemies[ei].w div 2,
enemies[ei].y + enemies[ei].h div 2, EXPTYPE_RED, false);
enemies[ei].active := false;
projectiles[i].active := false;
score := score + 5;
regular_enemies_destroyed := regular_enemies_destroyed + 1;
if (regular_enemies_destroyed mod 5) = 0 then
begin
slot := find_free_circular();
if slot <> -1 then
begin
circular_cx[slot] := 100 + (rand() mod (SCALE_W - 200));
circular[slot].x := circular_cx[slot];
circular[slot].y := -128;
circular[slot].angle := 0.0;
circular[slot].direction := 1;
circular[slot].active := true;
end;
end;
end;
end;
end;
{ check against circular enemies }
for ci := 0 to MAX_CIRCULAR - 1 do
begin
if circular[ci].active and projectiles[i].active then
begin
crect_x := circular[ci].x - 64;
crect_y := circular[ci].y - 64;
if aabb(projectiles[i].x, projectiles[i].y, projectiles[i].w, projectiles[i].h,
crect_x, crect_y, 128, 128) then
begin
add_explosion(circular[ci].x, circular[ci].y, EXPTYPE_BLUE, false);
circular[ci].active := false;
projectiles[i].active := false;
score := score + 10;
end;
end;
end;
end;
end;
end;
{ update regular enemies }
for ei := 0 to MAX_ENEMIES - 1 do
begin
if enemies[ei].active then
begin
enemies[ei].y := enemies[ei].y + 2;
if enemies[ei].y > SCALE_H then
begin
add_explosion(enemies[ei].x + enemies[ei].w div 2, SCALE_H - 20, EXPTYPE_RED, true);
enemies[ei].active := false;
end
else if aabb(player_x, player_y, player_w, player_h,
enemies[ei].x, enemies[ei].y, enemies[ei].w, enemies[ei].h) then
begin
add_explosion(player_x + player_w div 2, player_y + player_h div 2, EXPTYPE_RED, true);
enemies[ei].active := false;
player_die := true;
end;
end;
end;
{ update circular enemies }
for ci := 0 to MAX_CIRCULAR - 1 do
begin
if circular[ci].active then
begin
circular[ci].angle := circular[ci].angle + 0.05;
new_cx := circular_cx[ci] + 100.0 * cos(circular[ci].angle);
circular[ci].x := trunc(new_cx);
circular[ci].y := circular[ci].y + circular[ci].direction * 2;
if circular[ci].y > SCALE_H then circular[ci].direction := -1;
if circular[ci].y < 0 then circular[ci].direction := 1;
crect_x := circular[ci].x - 64;
crect_y := circular[ci].y - 64;
if aabb(player_x, player_y, player_w, player_h,
crect_x, crect_y, 128, 128) then
begin
add_explosion(player_x + player_w div 2, player_y + player_h div 2, EXPTYPE_RED, true);
circular[ci].active := false;
player_die := true;
end;
end;
end;
{ update stars }
for i := 0 to MAX_STARS - 1 do
begin
stars[i].y := stars[i].y + stars[i].speed;
if stars[i].y > SCALE_H then
begin
stars[i].y := 0;
stars[i].x := rand() mod SCALE_W;
end;
end;
{ spawn enemies }
if ticks - last_spawn_time > 1000 then
begin
slot := find_free_enemy();
if slot <> -1 then
begin
enemies[slot].x := rand() mod (SCALE_W - 64);
enemies[slot].y := -64;
enemies[slot].w := 64;
enemies[slot].h := 64;
enemies[slot].active := true;
end;
last_spawn_time := ticks;
end;
end;
{ ---- rendering ---- }
procedure render_game;
var
i: integer;
brightness: integer;
r1, g1, b1: integer;
begin
{ draw explosions }
draw_explosions;
{ draw stars }
for i := 0 to MAX_STARS - 1 do
begin
brightness := 200 + 55 * (stars[i].speed - 1);
if brightness > 255 then brightness := 255;
sdl_set_draw_color(renderer, brightness, brightness, brightness, 255);
sdl_draw_point(renderer, stars[i].x, stars[i].y);
end;
if game_state = STATE_PLAYING then
begin
{ draw player ship }
if player_die = false then
begin
if ship_tex <> -1 then
sdl_render_texture(renderer, ship_tex, -1, -1, -1, -1,
player_x - 8, player_y - 8, 64, 64)
else
draw_gradient_triangle(player_x + player_w div 2, player_y,
player_y + player_h);
end;
{ draw projectiles }
for i := 0 to MAX_PROJECTILES - 1 do
if projectiles[i].active then
begin
if particle_tex <> -1 then
sdl_render_texture(renderer, particle_tex, -1, -1, -1, -1,
projectiles[i].x, projectiles[i].y,
projectiles[i].w, projectiles[i].h)
else
begin
sdl_set_draw_color(renderer, 255, 255, 255, 255);
sdl_fill_rect(renderer, projectiles[i].x, projectiles[i].y,
projectiles[i].w, projectiles[i].h);
end;
end;
{ draw regular enemies }
for i := 0 to MAX_ENEMIES - 1 do
if enemies[i].active then
begin
if asteroid_tex <> -1 then
sdl_render_texture(renderer, asteroid_tex, -1, -1, -1, -1,
enemies[i].x, enemies[i].y,
enemies[i].w, enemies[i].h)
else
draw_gradient_diamond(enemies[i].x + enemies[i].w div 2,
enemies[i].y + enemies[i].h div 2,
enemies[i].w div 2, enemies[i].h div 2);
end;
{ draw circular enemies }
for i := 0 to MAX_CIRCULAR - 1 do
if circular[i].active then
begin
if saucer_tex <> -1 then
sdl_render_texture(renderer, saucer_tex, -1, -1, -1, -1,
circular[i].x - 64, circular[i].y - 64, 128, 128)
else
draw_gradient_circle(circular[i].x, circular[i].y, 64);
end;
end;
{ draw HUD }
sdl_draw_text(renderer, font_id, 'Lives: ' + inttostr(lives), 10, 10, 255, 255, 255, 255);
sdl_draw_text(renderer, font_id, 'Score: ' + inttostr(score), 10, 30, 255, 255, 255, 255);
{ death message }
if game_state = STATE_DIED then
begin
if spacelogo_tex <> -1 then
sdl_render_texture(renderer, spacelogo_tex, -1, -1, -1, -1, 0, 0, SCALE_W, SCALE_H);
r1 := rand() mod 255;
g1 := rand() mod 255;
b1 := rand() mod 255;
sdl_set_draw_color(renderer, 0, 0, 0, 255);
sdl_fill_rect(renderer, 470, 530, 500, 40);
sdl_draw_text(renderer, font_id, 'You Died! Press ENTER to continue', 490, 538, r1, g1, b1, 255);
end;
{ game over message }
if game_state = STATE_GAMEOVER then
begin
if spacelogo_tex <> -1 then
sdl_render_texture(renderer, spacelogo_tex, -1, -1, -1, -1, 0, 0, SCALE_W, SCALE_H);
r1 := rand() mod 255;
g1 := rand() mod 255;
b1 := rand() mod 255;
sdl_set_draw_color(renderer, 0, 0, 0, 255);
sdl_fill_rect(renderer, 470, 530, 500, 40);
sdl_draw_text(renderer, font_id, 'Game Over! Press ENTER to restart', 490, 538, r1, g1, b1, 255);
end;
end;
{ ---- intro screen ---- }
procedure render_intro;
var
r1, g1, b1: integer;
begin
{ draw background bitmap }
if spacelogo_tex <> -1 then
sdl_render_texture(renderer, spacelogo_tex, -1, -1, -1, -1, 0, 0, 1440, 1080)
else
begin
sdl_set_draw_color(renderer, 0, 0, 0, 255);
sdl_clear(renderer);
end;
{ draw logo bitmap }
if logo_tex <> -1 then
sdl_render_texture(renderer, logo_tex, -1, -1, -1, -1, 0, 0, SCALE_W, SCALE_H);
r1 := rand() mod 255;
g1 := rand() mod 255;
b1 := rand() mod 255;
sdl_draw_text(renderer, font_id, 'Press ENTER to Start', 640, 700, r1, g1, b1, 255);
sdl_draw_text(renderer, font_id, 'Arrow Keys = Move Space = Shoot', 584, 740, 180, 180, 180, 255);
end;
{ ---- SDL init / cleanup ---- }
procedure init_sdl;
var
result: integer;
begin
result := sdl_init();
if result <> 0 then
begin
writeln('failed to init SDL');
halt(EXIT_FAILURE);
end;
window_id := sdl_create_window('Space Shooter', 100, 100, WIDTH, HEIGHT, SDL_WINDOW_SHOWN);
if window_id = -1 then
begin
writeln('Could not create window');
sdl_quit();
halt(EXIT_FAILURE);
end;
renderer := sdl_create_renderer(window_id, -1, SDL_RENDERER_ACCELLERATED);
if renderer = -1 then
begin
writeln('Could not create renderer');
sdl_destroy_window(window_id);
sdl_quit();
halt(EXIT_FAILURE);
end;
target_id := sdl_create_render_target(renderer, SCALE_W, SCALE_H);
if target_id = -1 then
begin
writeln('Could not create render target');
sdl_destroy_renderer(renderer);
sdl_destroy_window(window_id);
sdl_quit();
halt(EXIT_FAILURE);
end;
sdl_set_draw_color(renderer, 0, 0, 0, 255);
if sdl_init_text() <> 1 then
begin
writeln('Font subsystem failed to load');
halt(EXIT_FAILURE);
end;
font_id := sdl_load_font(fontfile, 14);
if font_id = -1 then
begin
writeln('Failed to open: ', fontfile);
halt(EXIT_FAILURE);
end;
logo_tex := sdl_load_texture(renderer, logofile);
if logo_tex = -1 then
writeln('Warning: could not load ', logofile);
spacelogo_tex := sdl_load_texture(renderer, spacelogofile);
if spacelogo_tex = -1 then
writeln('Warning: could not load ', spacelogofile);
ship_tex := sdl_load_texture_color_key(renderer, shipfile);
if ship_tex = -1 then
writeln('Warning: could not load ', shipfile);
asteroid_tex := sdl_load_texture_color_key(renderer, asteroidfile);
if asteroid_tex = -1 then
writeln('Warning: could not load ', asteroidfile);
saucer_tex := sdl_load_texture_color_key(renderer, saucerfile);
if saucer_tex = -1 then
writeln('Warning: could not load ', saucerfile);
particle_tex := sdl_load_texture_color_key(renderer, particlefile);
if particle_tex = -1 then
writeln('Warning: could not load ', particlefile);
end;
procedure cleanup;
begin
if ship_tex <> -1 then sdl_destroy_texture(ship_tex);
if asteroid_tex <> -1 then sdl_destroy_texture(asteroid_tex);
if saucer_tex <> -1 then sdl_destroy_texture(saucer_tex);
if particle_tex <> -1 then sdl_destroy_texture(particle_tex);
if logo_tex <> -1 then sdl_destroy_texture(logo_tex);
if spacelogo_tex <> -1 then sdl_destroy_texture(spacelogo_tex);
sdl_destroy_render_target(target_id);
sdl_destroy_renderer(renderer);
sdl_destroy_window(window_id);
sdl_quit_text();
sdl_quit();
end;
{ ---- main loop ---- }
var
prev_time, cur_time: integer;
begin
init_sdl;
seed_random;
game_state := STATE_INTRO;
lives := 5;
score := 0;
clear_all_entities;
init_stars;
prev_time := sdl_get_ticks();
running := true;
while running = true do
begin
{ poll events }
while sdl_poll_event() <> 0 do
begin
event_type_result := sdl_get_event_type();
if event_type_result = SDL_QUIT then
running := false;
if event_type_result = SDL_KEYDOWN then
begin
key := sdl_get_key_code();
if key = SDLK_ESCAPE then
running := false;
if key = SDLK_RETURN then
begin
if game_state = STATE_INTRO then
reset_game
else if game_state = STATE_DIED then
reset_round
else if game_state = STATE_GAMEOVER then
reset_game;
end;
end;
end;
{ timed update at ~60fps }
cur_time := sdl_get_ticks();
if cur_time - prev_time >= 15 then
begin
prev_time := cur_time;
if game_state = STATE_PLAYING then
update_game;
end;
{ render }
sdl_set_render_target(renderer, target_id);
sdl_set_draw_color(renderer, 0, 0, 0, 255);
sdl_clear(renderer);
if game_state = STATE_INTRO then
render_intro
else
render_game;
sdl_present_scaled(renderer, target_id, WIDTH, HEIGHT);
sdl_delay(16);
end;
cleanup;
end.