1. Einführung

Wir wollten ein kleines Outrun Deluxe in EGSL (Easy Gaming Scripting Language von Markus Mangold) umsetzen. Ziel war es:

  • Eine perspektivische Straße
  • Spielerauto, Gegnerautos
  • Kurven und Drift
  • HUD (Zeit, Geschwindigkeit)
  • Deko-Elemente wie Palmen
  • Hintergrundmusik

EGSL eignet sich gut für einfache 2D-Spiele mit Sprites, Rechtecken und Kreisen. Für perspektivische 3D-Effekte gibt es aber Limitierungen, die wir im Verlauf kennenlernen werden.

2. Grundstruktur

Zunächst legten wir die Spielvariablen und Einstellungen fest:

-- Fenstergröße
W = 960
H = 600

-- Strecke
ROAD_W = 2000
SEG_LEN = 5
DRAW_DIST = 150
CAM_DEPTH = 0.84

-- Geschwindigkeit
MAX_SPEED = 400
ACCEL = 2
BRAKE = 4
DECEL = 1
DRIFT_FACTOR = 0.1

-- Spielerzustand
speed = 0
playerX = 0
pos = 0
lapTime = 75.0

-- Farben Himmel
skyTop = {r=255,g=120,b=120}
skyBottom = {r=255,g=200,b=120}

-- Arrays
track = {}
cars = {}

-- Sprites
playerSprite = nil
enemySprite = nil
palmSprite = nil

3. Strecke und Gegner erzeugen

Wir erzeugten die Straße als Tabelle von Segmenten. Jedes Segment enthält:

  • curve → Kurvenwert
  • y → Höhenprofil
  • color → Straßenfarbe
  • deco → Zufällige Deko (Palmen)
function createTrack()
   for i = 0,799 do
      local s = {}
      s.curve = math.sin(i*0.04)*2 + math.sin(i*0.01)*4
      s.y = math.sin(i*0.02)*50
      s.color = (i % 2 == 0) and {r=70,g=70,b=70} or {r=60,g=60,b=60}
      s.deco = (math.random(0,4) == 0)
      table.insert(track, s)
   end
end

 Gegnerautos werden zufällig auf der Strecke verteilt:

function spawnCars()
   for i = 1,20 do
      local c = {}
      c.z = math.random() * (#track * SEG_LEN)
      c.x = math.random()*2 - 1
      c.speed = MAX_SPEED*0.6 + math.random()*50
      c.sprite = enemySprite
      table.insert(cars, c)
   end
end

4. Spielerbewegung

Die Spielerbewegung berücksichtigt:

  • Geschwindigkeit, Beschleunigung, Bremsen
  • Drift basierend auf Kurven und Geschwindigkeit
  • Kollisionen mit Gegnerautos

function update()
   -- Zeit
   lapTime = lapTime - 0.02
   if lapTime <= 0 then

          return true

    end

    -- Geschwindigkeit
    if keystate(k_up) then

         speed = speed + ACCEL
    elseif keystate(k_down) then

         speed = speed - BRAKE
     else

        speed = speed - DECEL

     end
     speed = math.max(0, math.min(speed, MAX_SPEED))

     -- Drift
    if keystate(k_left) then

          playerX = playerX - DRIFT_FACTOR*(speed/MAX_SPEED)

    end
    if keystate(k_right) then

           playerX = playerX + DRIFT_FACTOR*(speed/MAX_SPEED)

    end

     -- Position
    pos = pos + speed
    if pos >= #track*SEG_LEN then
        pos = 0
        lapTime = lapTime + 20
    end

    -- Gegnerbewegung
    for _,c in ipairs(cars) do
         c.z = c.z + c.speed
        if c.z > #track*SEG_LEN then

             c.z = c.z - #track*SEG_LEN

        end
        if math.abs(c.z-pos) < 20 and math.abs(c.x-playerX) < 0.2 then
              speed = speed * 0.6
       end
   end

   return false
end

5. Projektion

Für die pseudo-3D-Perspektive berechnen wir die Position auf dem Bildschirm:

function project(x,y,z)
   local scale = CAM_DEPTH / z
   local sx = (1 + scale*x) * W/2
   local sy = (1 - scale*y) * H/2
   local sw = scale * ROAD_W * W/2
   return sx,sy,sw,scale
end

Hinweis: Es gibt eine Falle: Wenn z 0 wird, gab es zuvor Division by zero. Diese muss abgefangen werden, z.B. if z <= 0 then z = 0.1 end.

6. Rendern der Straße

Hier traten die größten Probleme auf:

  • Mit box()/fillbox() konnten wir nur rechteckige Streifen zeichnen.
  • Die Straße wurde oft nur als schmale Streifen dargestellt, Grünflächen übermalten sie teilweise.
  • Kurven waren kaum sichtbar, manchmal wirkte alles grau.
  • Das liegt an Limitierungen von EGSL: Es gibt keine Polygon-/Dreiecksfunktionen (kein filltriangle()), nur Rechtecke und Linien.

Beispielhafte Pseudo-Perspektive (noch stark vereinfacht):

function render()
   cls()
   drawGradient() -- Himmel

   local base = math.floor(pos/SEG_LEN)
   local camH = 100
   local x, dx = 0, 0

   for n = 0,DRAW_DIST do
       local seg = track[(base+n) % #track + 1]
       local z1 = (n*SEG_LEN) - (pos % SEG_LEN)
       if z1 <= 0 then

           z1 = 0.1

       end

       local sx,sy,sw,_ = project((playerX-x)*ROAD_W, camH-seg.y, z1)
       sx, sy, sw = math.floor(sx+0.5), math.floor(sy+0.5), math.floor(sw+0.5)

       -- Straße
       colour(seg.color.r, seg.color.g, seg.color.b)
       box(sx-sw, sy, sx+sw, H) -- untere Hälfte des Bildschirms

       dx = dx + seg.curve
       x = x + dx
    end
end

Probleme:

  • Untere Bildschirmhälfte (Straße) wird oft schwarz.
  • Kurven nur schwach sichtbar.
  • EGSL kann keine echten perspektivischen Dreiecke zeichnen → Straße wirkt nur wie Rechteckstreifen.

7. HUD, Gegner, Spieler und Deko

Nachdem die Projektion der Straße berechnet ist, werden die Overlay-Elemente—Gegnerautos, Spielerauto, Dekorationen und HUD—darüber gezeichnet.

Gegnerautos

  • Werden auf den berechneten Positionen mit putimage() gezeichnet:
for _, c in ipairs(cars) do
   local dz = c.z - pos
   if dz < 0 then
       dz = dz + #track*SEG_LEN
   end
   if dz > 0 and dz < DRAW_DIST*SEG_LEN then
       local sx, sy, sw, scale = project(c.x*ROAD_W, 100, dz)
       putimage(sx, sy-20, enemySprite)
   end
end
  • Bewegung: Gegner fahren entlang der Strecke und „wrappen“, wenn sie das Ende erreichen.
  • Kollision: Verringert die Geschwindigkeit des Spielers, wenn er nahe an einem Gegner ist.
 

Spielerauto

  • Wird immer unten in der Bildschirmmitte gezeichnet:
putimage(W/2, H-60, playerSprite)
 

Dekorationen (Palmen)

  • Werden auf bestimmten Streckensegmenten (seg.deco) zufällig platziert:
if seg.deco and n % 10 == 0 then
   putimage(sx + sw*1.2, sy - 50, palmSprite)
   putimage(sx - sw*1.5, sy - 50, palmSprite)
end
Zweck: Visuelle Abwechslung und Orientierungshilfe an den Straßenrändern.
 

HUD (Zeit und Geschwindigkeit)

  • Wird mit bmptext() und einer Bitmap-Schrift dargestellt:
bmptext(20, 20, "TIME: "..math.floor(lapTime), myfont)
bmptext(20, 50, "SPEED: "..math.floor(speed), myfont)
Zusammenfassung:
  • Diese Elemente werden 2D über der Straße gezeichnet.
  • In EGSL lassen sie sich relativ einfach umsetzen.
  • Die größte visuelle Einschränkung bleibt die pseudo-3D-Straße, aber Gegner, Spieler, Dekorationen und HUD funktionieren korrekt.
 

8. Musik

function initMusic()
   gamesound = loadsound("Blaster.wav")
   playsound(gamesound,16,2,true)
end

9. Main-Loop

function main()
   openwindow(W,H,32,"OUTRUN DELUXE")
   math.randomseed(os.time())
   colorkey(0,0,0)
   KeysConst() -- Tasten

   createTrack()
   spawnCars()
   initMusic()

   local gameover = false
   repeat
   key = getkey()
   if key == k_esc then

       break

   end
   gameover = update()
   render()
   sync()
   until key == k_esc or gameover

   closewindow()
end

main()

10. Fazit: EGSL für Outrun?

  • Vorteile von EGSL: Einfach, gut für 2D-Sprites, Rechtecke, Linien, einfache Spiele.
  • Probleme bei perspektivischer Straße:
    • Keine Polygon- oder Dreiecks-Funktionen (filltriangle())
    • Rechtecke können keine echte perspektivische Straße erzeugen
    • Kurven, Grünflächen und Streifen überlappen, Straße wirkt „schmal und flach“
  • Division by Zero muss vorsichtig abgefangen werden
  • Realistische Outrun-Optik mit Pseudo-3D ist in EGSL nicht möglich

Schlussfolgerung:

EGSL ist für ein echtes Outrun Deluxe mit perspektivischer Straße leider nicht geeignet.
Wer die Straße wirklich perspektivisch darstellen möchte, muss eine Engine mit Polygon-/Dreiecksfunktionen verwenden, z. B. LOVE2D, Unity oder Godot.