Jelajahi Sumber

Enhance the Space Invaders example: bombs, animation, autofire

Eric Streit 3 minggu lalu
induk
melakukan
8521014ebe
3 mengubah file dengan 176 tambahan dan 82 penghapusan
  1. 11 2
      modula2/ANALYSIS.md
  2. TEMPAT SAMPAH
      modula2/examples/invaders/invaders
  3. 165 80
      modula2/examples/invaders/invaders.mod

+ 11 - 2
modula2/ANALYSIS.md

@@ -25,8 +25,10 @@ Verified by building and running on this machine (X11 session):
   raw GL path (magenta `glClearColor`/`glClear` visible in `Window2`).
   `Essai2` (qualified imports) and `Essai3` build. `invaders` (new, no
   assets) is a playable Space Invaders game using only `tigrFillRect` +
-  the built-in font; verified rendering all palette colors and autonomous
-  invader motion. `essai4.mod` is
+  the built-in font: two-frame invader animation, accelerating movement,
+  aimed multiple bombs, bullet-vs-bomb scoring and autofire. Verified in
+  live captures: bombs (orange `#FFA028`) raining on every shot and sprite
+  frames toggling between frames. `essai4.mod` is
   not a tigr example: it is a standalone PIM-style bit-twiddling test
   (`SHIFT`/`AND`/`OR` on `CARDINAL`) that the ISO dialect does not accept -
   left as is.
@@ -196,6 +198,13 @@ GM2 gotchas encountered:
 - Adjacent string literals are **not** concatenated under ISO - keep strings
   on one line (done for the `fxShader`).
 - `SHORTREAL` literals (`1.0`) pass straight into C `float` parameters.
+- `tigrTime()` is **not** an absolute clock: each call returns the number of
+  seconds *since the previous call* (i.e. a per-frame delta; the first call
+  returns 0). Use it directly as `dt := tigrTime();` per frame (as
+  `demo.mod` and `shader.mod` do) - do not subtract two calls to build a
+  "now" you then difference, or timers effectively never fire (seen in the
+  first `invaders` version: `dt = now - lastTime` collapsed to ~0 and bombs
+  never spawned).
 
 ## Notes / minor oddities
 

TEMPAT SAMPAH
modula2/examples/invaders/invaders


+ 165 - 80
modula2/examples/invaders/invaders.mod

@@ -2,8 +2,8 @@ MODULE invaders;
 
 (* Simple "Space Invaders" using the tigr Modula-2 binding.
    No assets: everything is drawn with tigrFillRect. Built-in font.
-   Controls: LEFT/RIGHT move, SPACE fire, R restart (after game over),
-   ESC quit.
+   Controls: LEFT/RIGHT move, SPACE hold to autofire, R restart
+   (after game over), ESC quit.
 
    Compilation (from modula2/):
        gcc -c tigr.c
@@ -21,20 +21,22 @@ FROM helper IMPORT tigrRGB;
 CONST
     winW = 320;
     winH = 240;
-    invRows = 4;
-    invCols = 8;
-    cell    = 16;          (* invader sprite box *)
-    playerW = 24;
-    playerH = 6;
-    bulletW = 3;
-    bulletH = 9;
-    bombW   = 3;
-    bombH   = 8;
+    invRows   = 4;
+    invCols   = 8;
+    cell      = 16;          (* invader sprite box *)
+    maxBombs  = 5;
+    playerW   = 24;
+    playerH   = 6;
+    bulletW   = 3;
+    bulletH   = 9;
+    bombW     = 3;
+    bombH     = 8;
     bulletSpeed = 6;
-    bombSpeed   = 3;
+    bombSpeed   = 4;
     moveStep    = 4;
-    dropStep    = 10;
+    dropStep    = 12;
     border      = 4;
+    autofire    = 0.3;
 
 TYPE
     State = (s_play, s_win, s_lose);
@@ -44,20 +46,21 @@ VAR
     alive           : ARRAY [0..invRows - 1] OF ARRAY [0..invCols - 1] OF BOOLEAN;
     gx, gy          : CARDINAL;          (* invader grid origin *)
     dir             : INTEGER;           (* -1 left, +1 right *)
+    frame           : CARDINAL;          (* 0/1 invader sprite frame *)
     moveAcc, moveInt : SHORTREAL;
+    fireAcc         : SHORTREAL;
     playerX, playerY : CARDINAL;
     bulletActive    : BOOLEAN;
     bulletX, bulletY : CARDINAL;
-    bombActive      : BOOLEAN;
-    bombX, bombY    : CARDINAL;
+    bombFlags       : ARRAY [0..maxBombs - 1] OF BOOLEAN;
+    bombX, bombY    : ARRAY [0..maxBombs - 1] OF CARDINAL;
     score, lives    : CARDINAL;
     seed            : CARDINAL;          (* tiny LCG *)
     state           : State;
-    lastTime, now   : SHORTREAL;
-    dt              : SHORTREAL;
-    black, white, red, green, yellow : TPixelType;
+    dt              : SHORTREAL;         (* tigrTime: delta since last call *)
+    black, white, red, green, yellow, bombColor : TPixelType;
     palette         : ARRAY [0..invRows - 1] OF TPixelType;
-    row, col, n     : CARDINAL;
+    row, col, n, slot : CARDINAL;
     scoreText       : ARRAY [0..20] OF CHAR;
     msgbuf          : ARRAY [0..60] OF CHAR;
 
@@ -100,13 +103,17 @@ VAR
         gx := 32;
         gy := 20;
         dir := 1;
+        frame := 0;
         moveAcc := 0.0;
-        moveInt := 0.55;
+        moveInt := 0.5;
+        fireAcc := 0.0;
         score := 0;
         lives := 3;
         playerX := (winW - playerW) / 2;
         bulletActive := FALSE;
-        bombActive := FALSE;
+        FOR slot := 0 TO maxBombs - 1 DO
+            bombFlags[slot] := FALSE
+        END;
         seed := 1;
         state := s_play;
         FOR r := 0 TO invRows - 1 DO
@@ -158,25 +165,57 @@ VAR
                 gx := gx + moveStep
             END
         END;
+        frame := 1 - frame;
 
-        (* Speeds up as invaders are destroyed, and may drop a bomb. *)
+        (* Speeds up as invaders are destroyed. *)
         cnt := CountAlive();
-        moveInt := 0.55 - (SHORTREAL(CountAlive() - 1)
-                 * 0.35) / SHORTREAL(invRows * invCols - 1);
+        IF cnt > 0 THEN
+            moveInt := 0.5 - (SHORTREAL(cnt - 1)
+                     * 0.38) / SHORTREAL(invRows * invCols - 1)
+        END
+    END MoveInvaders;
+
+    PROCEDURE TrySpawnBomb;
+
+    (* Drops a bomb from a surviving invader, some aimed at the player. *)
+
+        VAR
+            tgtCol, r2 : CARDINAL;
+            tgt        : INTEGER;
+            aim        : BOOLEAN;
+
+    BEGIN
+        slot := 0;
+        WHILE (slot < maxBombs) AND bombFlags[slot] DO
+            slot := slot + 1
+        END;
+        IF slot = maxBombs THEN
+            RETURN
+        END;
         seed := (seed * 1103515245 + 12345) MOD 2147483648;
-        IF (NOT bombActive) AND (cnt > 0) AND (seed MOD 3 = 0) THEN
-            col := seed MOD invCols;
-            row := 0;
-            WHILE (row < invRows) AND (NOT alive[row][col]) DO
-                row := row + 1
-            END;
-            IF row < invRows THEN
-                bombActive := TRUE;
-                bombX := gx + col * cell + (cell - bombW) / 2;
-                bombY := gy + row * cell + cell
+        aim := seed MOD 4 = 0;
+        IF aim THEN
+            tgt := INTEGER(playerX + playerW / 2) - INTEGER(gx);
+            IF tgt < 0 THEN
+                tgtCol := 0
+            ELSIF tgt >= INTEGER(invCols * cell) THEN
+                tgtCol := invCols - 1
+            ELSE
+                tgtCol := tgt / cell
             END
+        ELSE
+            tgtCol := seed MOD invCols
+        END;
+        r2 := 0;
+        WHILE (r2 < invRows) AND (NOT alive[r2][tgtCol]) DO
+            r2 := r2 + 1
+        END;
+        IF r2 < invRows THEN
+            bombFlags[slot] := TRUE;
+            bombX[slot] := gx + tgtCol * cell + (cell - bombW) / 2;
+            bombY[slot] := gy + r2 * cell + cell
         END
-    END MoveInvaders;
+    END TrySpawnBomb;
 
     PROCEDURE Shoot;
 
@@ -191,9 +230,9 @@ VAR
     PROCEDURE MoveBullet;
 
         VAR
-            r, c        : CARDINAL;
-            ix, iy      : CARDINAL;
-            overlaps    : BOOLEAN;
+            r, c, ix, iy : CARDINAL;
+            overlaps     : BOOLEAN;
+            slot2        : CARDINAL;
 
     BEGIN
         IF bulletActive THEN
@@ -202,6 +241,8 @@ VAR
             ELSE
                 bulletActive := FALSE
             END;
+
+            (* Versus invaders. *)
             overlaps := FALSE;
             r := 0;
             WHILE (r < invRows) AND (NOT overlaps) DO
@@ -224,51 +265,87 @@ VAR
             IF overlaps THEN
                 bulletActive := FALSE
             END;
+
+            (* Versus bombs (shoot them down for +5). *)
+            IF bulletActive THEN
+                slot2 := 0;
+                WHILE slot2 < maxBombs DO
+                    IF bombFlags[slot2]
+                       AND (bulletX + bulletW > bombX[slot2])
+                       AND (bulletX < bombX[slot2] + bombW)
+                       AND (bulletY + bulletH > bombY[slot2])
+                       AND (bulletY < bombY[slot2] + bombH) THEN
+                        bombFlags[slot2] := FALSE;
+                        score := score + 5;
+                        bulletActive := FALSE;
+                        slot2 := maxBombs
+                    ELSE
+                        slot2 := slot2 + 1
+                    END
+                END
+            END;
+
             IF CountAlive() = 0 THEN
                 state := s_win
             END
         END
     END MoveBullet;
 
-    PROCEDURE MoveBomb;
+    PROCEDURE MoveBombs;
 
     BEGIN
-        IF bombActive THEN
-            bombY := bombY + bombSpeed;
-            IF bombY >= winH THEN
-                bombActive := FALSE
-            ELSIF (bombX + bombW > playerX) AND (bombX < playerX + playerW)
-                  AND (bombY + bombH > playerY) AND (bombY < playerY + playerH) THEN
-                bombActive := FALSE;
-                IF lives > 1 THEN
-                    lives := lives - 1;
-                    playerX := (winW - playerW) / 2
-                ELSE
-                    lives := 0;
-                    state := s_lose
+        FOR slot := 0 TO maxBombs - 1 DO
+            IF bombFlags[slot] THEN
+                bombY[slot] := bombY[slot] + bombSpeed;
+                IF bombY[slot] >= winH THEN
+                    bombFlags[slot] := FALSE
+                ELSIF (bombX[slot] + bombW > playerX)
+                      AND (bombX[slot] < playerX + playerW)
+                      AND (bombY[slot] + bombH > playerY)
+                      AND (bombY[slot] < playerY + playerH) THEN
+                    bombFlags[slot] := FALSE;
+                    IF lives > 1 THEN
+                        lives := lives - 1;
+                        playerX := (winW - playerW) / 2
+                    ELSE
+                        lives := 0;
+                        state := s_lose
+                    END
                 END
             END
         END
-    END MoveBomb;
+    END MoveBombs;
 
     PROCEDURE DrawInvader(x, y : CARDINAL; color : TPixelType);
 
+    (* Two classic invader frames, toggled on every move step. *)
+
     BEGIN
-        tigrFillRect(win, x + 3, y, cell - 6, 3, color);     (* antenna *)
-        tigrFillRect(win, x + 1, y + 3, cell - 2, 8, color); (* body *)
-        tigrFillRect(win, x + 4, y + 5, 2, 3, black);        (* left eye *)
-        tigrFillRect(win, x + 10, y + 5, 2, 3, black);       (* right eye *)
-        tigrFillRect(win, x, y + 11, 4, 3, color);           (* feet *)
-        tigrFillRect(win, x + 12, y + 11, 4, 3, color)
+        IF frame = 0 THEN
+            tigrFillRect(win, x + 3, y, 2, 3, color);       (* antenna up *)
+            tigrFillRect(win, x + 11, y, 2, 3, color);
+            tigrFillRect(win, x + 1, y + 3, cell - 2, 8, color); (* body *)
+            tigrFillRect(win, x + 4, y + 5, 2, 3, black);   (* eyes *)
+            tigrFillRect(win, x + 10, y + 5, 2, 3, black);
+            tigrFillRect(win, x, y + 11, 4, 3, color);      (* feet out *)
+            tigrFillRect(win, x + 12, y + 11, 4, 3, color)
+        ELSE
+            tigrFillRect(win, x + 3, y + 1, 2, 2, color);   (* antenna down *)
+            tigrFillRect(win, x + 11, y + 1, 2, 2, color);
+            tigrFillRect(win, x + 1, y + 3, cell - 2, 8, color);
+            tigrFillRect(win, x + 4, y + 5, 2, 3, black);
+            tigrFillRect(win, x + 10, y + 5, 2, 3, black);
+            tigrFillRect(win, x + 2, y + 11, 3, 3, color);  (* feet tucked *)
+            tigrFillRect(win, x + 11, y + 11, 3, 3, color)
+        END
     END DrawInvader;
 
     PROCEDURE Draw;
 
         VAR
-            r, c    : CARDINAL;
-            ix, iy  : CARDINAL;
-            tw      : INTEGER;
-            msgX    : CARDINAL;
+            r, c, ix, iy : CARDINAL;
+            tw           : INTEGER;
+            msgX         : CARDINAL;
 
     BEGIN
         tigrClear(win, black);
@@ -286,9 +363,13 @@ VAR
         IF bulletActive THEN
             tigrFillRect(win, bulletX, bulletY, bulletW, bulletH, white)
         END;
-        IF bombActive THEN
-            tigrFillRect(win, bombX, bombY, bombW, bombH, red)
+        FOR slot := 0 TO maxBombs - 1 DO
+            IF bombFlags[slot] THEN
+                tigrFillRect(win, bombX[slot], bombY[slot], bombW, bombH,
+                             bombColor)
+            END
         END;
+
         scoreText := "SCORE ";
         CardToText(score * 10, msgbuf);
         n := 6;
@@ -298,7 +379,8 @@ VAR
         END;
         scoreText[n] := 0C;
         tigrPrint(win, tfont, 6, 4, white, scoreText);
-        tigrPrint(win, tfont, 6, 14, yellow, "L/R move SPACE fire ESC quit");
+        tigrPrint(win, tfont, 6, 14, yellow,
+                  "L/R move SPACE fire ESC quit");
 
         IF state = s_win THEN
             tw := tigrTextWidth(tfont, "YOU WIN");
@@ -335,11 +417,12 @@ VAR
 
 BEGIN
     win := tigrWindow(winW, winH, "Space Invaders", 0);
-    black  := tigrRGB(0, 0, 0);
-    white  := tigrRGB(255, 255, 255);
-    red    := tigrRGB(255, 60, 60);
-    green  := tigrRGB(60, 230, 60);
-    yellow := tigrRGB(255, 220, 60);
+    black      := tigrRGB(0, 0, 0);
+    white      := tigrRGB(255, 255, 255);
+    red        := tigrRGB(255, 60, 60);
+    green      := tigrRGB(60, 230, 60);
+    yellow     := tigrRGB(255, 220, 60);
+    bombColor  := tigrRGB(255, 160, 40);
     palette[0] := yellow;
     palette[1] := green;
     palette[2] := red;
@@ -348,11 +431,8 @@ BEGIN
 
     StartGame;
 
-    lastTime := tigrTime();
     WHILE (NOT (tigrClosed(win) > 0)) AND (NOT (tigrKeyDown(win, TK_ESCAPE) > 0)) DO
-        now := tigrTime();
-        dt := now - lastTime;
-        lastTime := now;
+        dt := tigrTime();       (* seconds since the previous call *)
 
         IF state = s_play THEN
             (* Player movement *)
@@ -363,19 +443,24 @@ BEGIN
                (playerX + playerW < winW - border) THEN
                 playerX := playerX + 3
             END;
-            IF tigrKeyDown(win, TK_SPACE) > 0 THEN
-                Shoot
+            fireAcc := fireAcc + dt;
+            IF (tigrKeyDown(win, TK_SPACE) > 0)
+               OR ((tigrKeyHeld(win, TK_SPACE) > 0) AND (fireAcc >= autofire)) THEN
+                Shoot;
+                fireAcc := 0.0
             END;
 
-            (* Invader ticks *)
+            (* Invader ticks: move, then rain up to two bombs. *)
             moveAcc := moveAcc + dt;
             WHILE moveAcc >= moveInt DO
                 moveAcc := moveAcc - moveInt;
-                MoveInvaders
+                MoveInvaders;
+                TrySpawnBomb;
+                TrySpawnBomb
             END;
 
             MoveBullet;
-            MoveBomb
+            MoveBombs
         ELSIF tigrKeyDown(win, TK_SPACE) > 0 THEN
             StartGame
         END;