The Next Level

Our game, so far.

// My Game
// by Mr Green
// Created 2025/12/16

Symbol 0,"2__0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0P?0_0/P?0!P0,P0_0/P0,P0?P,00P0_0/P00P,0.P,00P0_0/P00P,0.P,00P0_0/P00P,0.P,00P!0!P!00P,0_0?P@0_0_0_0_0_0/P!0.P!0_0_0_0_0_0.P?0PP0P?0_0_0!PP0_0_0/P.0PP0P.0_0_0/PP0_0_0_0,PP0_0@P_P,0PP0P_P,0.P0_0_0@P0.P0_0_0@P0.P0_0_0@P0.P0@P0@P0@P0.PP0/P0@P0/PP0?P0/P0P!0P0/P0!P?0,P0@P0,P?0@P,0,P0@P0,P,0_0?P,0@P,";
Symbol 1,"2__0_0_0_0_0_0_0_0_0_0_0_0_0_0_00P,0_0_0@P_P,0_0?P_P,0?P0_0?P0?P,0@P0_0?P0_0/P0_0?P0_0/P?0!P?0_0_00P0!P0_0_0!P@0_0_0_0_0_0/P!0.P!0_0_0_0_0_0.P?0PP0P?0_0_0!PP0_0_0/P.0PP0P.0_0_0/PP0_0_0_0,PP0_0!P_P?0PP0P_P!0_0_0_0,PP0_0_0_0,PP0_0_0_0,PP0_0P0@P0_0PP0_0P0@P0_0PP0_0P0P!0P0_0PP0_0P0P0.P0P0_0PP0_0P0P0.P0P0_0P,0/P,0P!0P,0/P.0_0_0_0P.0_0_0_0PP";
Symbol 2,"2__0_0_0_0_0_0_0_0_0_0_0_0_0NN00NN0_0_0/NPQNNQPN0_0_0!NPQ.PN0_0_0!NPQ.PN0_0_0!NPQ.PN0_0_0!NPQ.PN0_0_0!NPQ.PN0_0_0!NPQ.PN0_0_0!NPQ.PN0_0_0!NPQ.PN0_0_0!NPQ.PN0_0_0!NPQ.PN0_0_0?NPPQ.PPN0_0_0.NPPQ.PPN0_0_0.NPPQ.PPN0_0_0.NPPQ.PPN0_0_0.NPQ!PN0_0_0.NPQ!PN0_0_0.NPQ!PN0_0_0,NPPQ!PPN0_0_00NPPQ!PPN0_0_00NPPQ!PPN0_0_00NPPQ!PPN0_0,P_P_P_P0,PS_S_S_SP0PS_S_S_S,PPSSR,S,R,S,RRS,R,S,R,SSP0S_S_S_S,0,S_S_S_S";
Symbol 3,"2__P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P/";
Symbol 4,"2__0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_z0_0_0_0020,20_0_0_00;0_0_0_0z0;1;0z0_0_0_0;0_0_0_0020,20_0_0_00z";

AntiAlias Off // Turn off AntiAlias, for Crispy Invaders


// Arrays
Dim EnemyAlive(20)

// Main Loop
GameRun=0
Repeat
  If GameRun==0 then Gosub .StartScreen
  If GameRun==1 then Gosub .InGame
Flip
Forever


// This is our Titlescreen
.StartScreen
CLS
ResetDraw

  ShowInvader=1
  If Wrap(Frames,0,20)<10 Then ShowInvader=0
  DrawImg 320,160,ShowInvader

  SetFontSize 32
  Text 320,240,"Titlescreen!",1
  SetFontSize 16
  Text 320,360,"Press Fire to Play",1
  
  If GamePad(ButtonA)
    Gosub StartGame
    GameRun=1
  Endif
Return


// This prepares our game when the A Button is hit
.StartGame
  Score=0
  PlayerX=320 // Middle of the screen
  PlayerY=440 // Bottom of the screen
  
  BulletX=0  // Reset player's bullet
  BulletY=0
  
  For BadGuy=1 to 20
    EnemyAlive(BadGuy)=1
  Next

Return


// This is our ingame loop
.InGame
CLS
ResetDraw

  // Starfield using Symbol 4
  Starfield 1,0,4

  // Player
  DrawImg PlayerX,PlayerY,2
  Speed=4
  If GamePad(ButtonLeft)>0.5 then PlayerX=PlayerX-Speed
  If GamePad(ButtonRight)>0.5 then PlayerX=PlayerX+Speed
  // Using >0.5 allows players to use a thumbstick, too!
  
  PlayerX=Limit(PlayerX,32,640-32)
  
  
  // Player's Bullet
  // Keep the Bullet going upwards
  BulletSpeed=16
  BulletY=Limit(BulletY-BulletSpeed, -100,480)

  // Draw the Bullet
  SetScale 0.2,0.5

  BulletCollide=DrawImg(BulletX,BulletY,3)

  // And reset things afterwards
  ResetDraw
  
  If BulletY<0 And GamePad(ButtonA)
    BulletX=PlayerX
    BulletY=PlayerY
    PlaySFX("Lazer_2")
  EndIf

  
  // Enemy Section
  
  // Reuse this to keep the invaders animated
  ShowInvader=1
  if Wrap(Frames,0,20)<10 then ShowInvader=0
  
  CircleSize=160
  // New Variable for Circle Position
  CircleX=320+Sin(Frames)*160

  Gap=15
  
  // A loop for 20 Bad Guys
  For BadGuy=1 to 20
    // If Alive
    If EnemyAlive(BadGuy)==1
    
      // Each enemy should be further around the loop.
      PlusAngle=BadGuy*Gap
      Angle=Wrap(Frames + PlusAngle,0,360)
      EnemyX=CircleX+Cos(Angle)*CircleSize
      EnemyY=200+Sin(Angle)*CircleSize
      EnemyColour=Wrap(BadGuy,0,7)
      
      // New collision code
      EnemyCollide=DrawImg(EnemyX,EnemyY,ShowInvader,EnemyColour)
      If Collide(BulletCollide,EnemyCollide)
        // If the bullet hits the Enemy

        // Turn off the Enemy
        EnemyAlive(BadGuy)=0
        // Make a Bang sound
        PlaySFX("Explode_Low_2")  
        // Gimme 5 points!
        Gimme 5,EnemyX,EnemyY      
        
        // Move the Bullet off the screen
        BulletY=-100
      EndIf // Collision
      
    EndIf // Alive
    
  Next // Each BadGuy


  
  // Quit button (Return)
  If GamePad(ButtonStart) then GameRun=0
    // We can hit the Return key to quit
    
  SetFontSize 16
  Text 320,16,Score,1
Return

The Game Stops!

Without any gameplay logic, our game becomes stuck after we destroy all of the enemies. There's nothing left to shoot, but the game just keeps running!

Let's fix this with a new Subroutine.

// This resets the current level
.NextLevel

Return

There's no real rule on where subroutines go inside GotoJSE, but in other languages people try to put them in an order so that jumps go upwards instead of down through the script.

I've placed this subroutine just above the .StartGame subroutine, since the two subroutines have a similar purpose.

In fact, we can even "pinch" a piece of .StartGame and put it inside .NextLevel

  For BadGuy=1 to 20
    EnemyAlive(BadGuy)=1
  Next

Take these three lines from the .StartGame subroutine, and move them into the NextLevel, and then inside StartGame place the line.

Gosub NextLevel

Now StartGame will do our first NextLevel for us!


This subroutine has "Next" in its name, but "Next" is already a command?

 

"NextLevel" is a whole word, and GotoJSE only reads whole words, not half words.

GotoJSE treats NextLevel as its own thing, like how our EnemyX variable isn't the same as EnemyY.

Counting

The next step is pretty easy.

The next step is pretty easy.

Count baddies and jump to the NextLevel subroutine if we need to.

Above the For BadGuy loop, set a BaddyCount variable to 0

BaddyCount=0

And inside the "If Alive!" area, just add one.

BaddyCount=BaddyCount+1
We're halfway there! This is easy!

 

So far, we've..

Set the count to 0
Counted as we draw each baddy

Now all we need to do is the Gosub bit.

We can put this underneath our BadGuy loop's Next. Just make space for it before the Quit Button bit.

If BaddyCount<1 then Gosub NextLevel

Now the invaders "blip" back to life again!

Our game, so far.

// My Game
// by Mr Green
// Created 2025/12/16

Symbol 0,"2__0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0P?0_0/P?0!P0,P0_0/P0,P0?P,00P0_0/P00P,0.P,00P0_0/P00P,0.P,00P0_0/P00P,0.P,00P!0!P!00P,0_0?P@0_0_0_0_0_0/P!0.P!0_0_0_0_0_0.P?0PP0P?0_0_0!PP0_0_0/P.0PP0P.0_0_0/PP0_0_0_0,PP0_0@P_P,0PP0P_P,0.P0_0_0@P0.P0_0_0@P0.P0_0_0@P0.P0@P0@P0@P0.PP0/P0@P0/PP0?P0/P0P!0P0/P0!P?0,P0@P0,P?0@P,0,P0@P0,P,0_0?P,0@P,";
Symbol 1,"2__0_0_0_0_0_0_0_0_0_0_0_0_0_0_00P,0_0_0@P_P,0_0?P_P,0?P0_0?P0?P,0@P0_0?P0_0/P0_0?P0_0/P?0!P?0_0_00P0!P0_0_0!P@0_0_0_0_0_0/P!0.P!0_0_0_0_0_0.P?0PP0P?0_0_0!PP0_0_0/P.0PP0P.0_0_0/PP0_0_0_0,PP0_0!P_P?0PP0P_P!0_0_0_0,PP0_0_0_0,PP0_0_0_0,PP0_0P0@P0_0PP0_0P0@P0_0PP0_0P0P!0P0_0PP0_0P0P0.P0P0_0PP0_0P0P0.P0P0_0P,0/P,0P!0P,0/P.0_0_0_0P.0_0_0_0PP";
Symbol 2,"2__0_0_0_0_0_0_0_0_0_0_0_0_0NN00NN0_0_0/NPQNNQPN0_0_0!NPQ.PN0_0_0!NPQ.PN0_0_0!NPQ.PN0_0_0!NPQ.PN0_0_0!NPQ.PN0_0_0!NPQ.PN0_0_0!NPQ.PN0_0_0!NPQ.PN0_0_0!NPQ.PN0_0_0!NPQ.PN0_0_0?NPPQ.PPN0_0_0.NPPQ.PPN0_0_0.NPPQ.PPN0_0_0.NPPQ.PPN0_0_0.NPQ!PN0_0_0.NPQ!PN0_0_0.NPQ!PN0_0_0,NPPQ!PPN0_0_00NPPQ!PPN0_0_00NPPQ!PPN0_0_00NPPQ!PPN0_0,P_P_P_P0,PS_S_S_SP0PS_S_S_S,PPSSR,S,R,S,RRS,R,S,R,SSP0S_S_S_S,0,S_S_S_S";
Symbol 3,"2__P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P/";
Symbol 4,"2__0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_z0_0_0_0020,20_0_0_00;0_0_0_0z0;1;0z0_0_0_0;0_0_0_0020,20_0_0_00z";

AntiAlias Off // Turn off AntiAlias, for Crispy Invaders


// Arrays
Dim EnemyAlive(20)

// Main Loop
GameRun=0
Repeat
  If GameRun==0 then Gosub .StartScreen
  If GameRun==1 then Gosub .InGame
Flip
Forever


// This is our Titlescreen
.StartScreen
CLS
ResetDraw

  ShowInvader=1
  If Wrap(Frames,0,20)<10 Then ShowInvader=0
  DrawImg 320,160,ShowInvader

  SetFontSize 32
  Text 320,240,"Titlescreen!",1
  SetFontSize 16
  Text 320,360,"Press Fire to Play",1
  
  If GamePad(ButtonA)
    Gosub StartGame
    GameRun=1
  Endif
Return



// This resets the current level
.NextLevel
  For BadGuy=1 to 20
    EnemyAlive(BadGuy)=1
  Next

Return


// This prepares our game when the A Button is hit
.StartGame
  Score=0
  PlayerX=320 // Middle of the screen
  PlayerY=440 // Bottom of the screen
  
  BulletX=0  // Reset player's bullet
  BulletY=0
  
  Gosub NextLevel
Return



// This is our ingame loop
.InGame
CLS
ResetDraw

  // Starfield using Symbol 4
  Starfield 1,0,4

  // Player
  DrawImg PlayerX,PlayerY,2
  Speed=4
  If GamePad(ButtonLeft)>0.5 then PlayerX=PlayerX-Speed
  If GamePad(ButtonRight)>0.5 then PlayerX=PlayerX+Speed
  // Using >0.5 allows players to use a thumbstick, too!
  
  PlayerX=Limit(PlayerX,32,640-32)
  
  
  // Player's Bullet
  // Keep the Bullet going upwards
  BulletSpeed=16
  BulletY=Limit(BulletY-BulletSpeed, -100,480)

  // Draw the Bullet
  SetScale 0.2,0.5

  BulletCollide=DrawImg(BulletX,BulletY,3)

  // And reset things afterwards
  ResetDraw
  
  If BulletY<0 And GamePad(ButtonA)
    BulletX=PlayerX
    BulletY=PlayerY
    PlaySFX("Lazer_2")
  EndIf

  
  // Enemy Section
  
  // Reuse this to keep the invaders animated
  ShowInvader=1
  if Wrap(Frames,0,20)<10 then ShowInvader=0
  
  CircleSize=160
  // New Variable for Circle Position
  CircleX=320+Sin(Frames)*160

  Gap=15

  BaddyCount=0
  // A loop for 20 Bad Guys
  For BadGuy=1 to 20
    // If Alive
    If EnemyAlive(BadGuy)==1
    
      BaddyCount=BaddyCount+1

      // Each enemy should be further around the loop.
      PlusAngle=BadGuy*Gap
      Angle=Wrap(Frames + PlusAngle,0,360)
      EnemyX=CircleX+Cos(Angle)*CircleSize
      EnemyY=200+Sin(Angle)*CircleSize
      EnemyColour=Wrap(BadGuy,0,7)
      
      // New collision code
      EnemyCollide=DrawImg(EnemyX,EnemyY,ShowInvader,EnemyColour)
      If Collide(BulletCollide,EnemyCollide)
        // If the bullet hits the Enemy

        // Turn off the Enemy
        EnemyAlive(BadGuy)=0
        // Make a Bang sound
        PlaySFX("Explode_Low_2")  
        // Gimme 5 points!
        Gimme 5,EnemyX,EnemyY      
        
        // Move the Bullet off the screen
        BulletY=-100
      EndIf // Collision
      
    EndIf // Alive
    
  Next // Each BadGuy

  If BaddyCount<1 then Gosub NextLevel

  
  // Quit button (Return)
  If GamePad(ButtonStart) then GameRun=0
    // We can hit the Return key to quit
    
  SetFontSize 16
  Text 320,16,Score,1
Return

Entering a Level

When we create a new level the enemies instantly "blip" onto the screen. That may be a little bit too chaotic!

Let's instead lower things into play, by making the invaders drop down from the top of the screen.

Our Circle of Invaders!

Our Circle of Invaders!

Gosh, look at all the maths we've done!
You should be proud of yourself!

 

Currently the invaders positions are all based on the Red X (Our pivot point).

So far its Y position has been fairly flat, at 200.

The plan, then, will be to have a new variable (DropDown) that starts off large when we start a .NextLevel

We'll then drop it down to 0, and we can simply subtract this from the Y position of our Red Pivot, so that the Red Pivot drops down from above.

Our plan looks like a Festive Bauble!

Our plan looks like a Festive Bauble!

Lower the Pivot

Let's start with that large value.

The whole screen is 480 pixels high, so if we make our large value 480, it's going to be "A whole screen away". That sounds like a good starting point.

We can drop this into the .NextLevel subroutine, so that it gets reset to the large number whenever a next level is reached.

DropDown=480

Inside InGame, where we've defined CircleX, we should create a new CircleY with the DropDown taken away.

We can add the reduction of DropDown here, too, since it only happens once per frame.

CircleX=320+Sin(Frames)*160
CircleY=200-DropDown
DropDown=Limit( DropDown-5 ,0,480)
  // Why not try changing this value, too.
    // The higher the number the faster the drop.
    // Make it slower to have the invaders creep into view!

And finally, change our EnemyY line to use CircleY instead of the hardcoded number 200.

EnemyY=CircleY+Sin(Angle)*CircleSize
They're coming from above!

They're coming from above!

The Invaders are Invading!!

Our game, so far.

// My Game
// by Mr Green
// Created 2025/12/16

Symbol 0,"2__0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0P?0_0/P?0!P0,P0_0/P0,P0?P,00P0_0/P00P,0.P,00P0_0/P00P,0.P,00P0_0/P00P,0.P,00P!0!P!00P,0_0?P@0_0_0_0_0_0/P!0.P!0_0_0_0_0_0.P?0PP0P?0_0_0!PP0_0_0/P.0PP0P.0_0_0/PP0_0_0_0,PP0_0@P_P,0PP0P_P,0.P0_0_0@P0.P0_0_0@P0.P0_0_0@P0.P0@P0@P0@P0.PP0/P0@P0/PP0?P0/P0P!0P0/P0!P?0,P0@P0,P?0@P,0,P0@P0,P,0_0?P,0@P,";
Symbol 1,"2__0_0_0_0_0_0_0_0_0_0_0_0_0_0_00P,0_0_0@P_P,0_0?P_P,0?P0_0?P0?P,0@P0_0?P0_0/P0_0?P0_0/P?0!P?0_0_00P0!P0_0_0!P@0_0_0_0_0_0/P!0.P!0_0_0_0_0_0.P?0PP0P?0_0_0!PP0_0_0/P.0PP0P.0_0_0/PP0_0_0_0,PP0_0!P_P?0PP0P_P!0_0_0_0,PP0_0_0_0,PP0_0_0_0,PP0_0P0@P0_0PP0_0P0@P0_0PP0_0P0P!0P0_0PP0_0P0P0.P0P0_0PP0_0P0P0.P0P0_0P,0/P,0P!0P,0/P.0_0_0_0P.0_0_0_0PP";
Symbol 2,"2__0_0_0_0_0_0_0_0_0_0_0_0_0NN00NN0_0_0/NPQNNQPN0_0_0!NPQ.PN0_0_0!NPQ.PN0_0_0!NPQ.PN0_0_0!NPQ.PN0_0_0!NPQ.PN0_0_0!NPQ.PN0_0_0!NPQ.PN0_0_0!NPQ.PN0_0_0!NPQ.PN0_0_0!NPQ.PN0_0_0?NPPQ.PPN0_0_0.NPPQ.PPN0_0_0.NPPQ.PPN0_0_0.NPPQ.PPN0_0_0.NPQ!PN0_0_0.NPQ!PN0_0_0.NPQ!PN0_0_0,NPPQ!PPN0_0_00NPPQ!PPN0_0_00NPPQ!PPN0_0_00NPPQ!PPN0_0,P_P_P_P0,PS_S_S_SP0PS_S_S_S,PPSSR,S,R,S,RRS,R,S,R,SSP0S_S_S_S,0,S_S_S_S";
Symbol 3,"2__P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P/";
Symbol 4,"2__0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_z0_0_0_0020,20_0_0_00;0_0_0_0z0;1;0z0_0_0_0;0_0_0_0020,20_0_0_00z";

AntiAlias Off // Turn off AntiAlias, for Crispy Invaders


// Arrays
Dim EnemyAlive(20)

// Main Loop
GameRun=0
Repeat
  If GameRun==0 then Gosub .StartScreen
  If GameRun==1 then Gosub .InGame
Flip
Forever


// This is our Titlescreen
.StartScreen
CLS
ResetDraw

  ShowInvader=1
  If Wrap(Frames,0,20)<10 Then ShowInvader=0
  DrawImg 320,160,ShowInvader

  SetFontSize 32
  Text 320,240,"Titlescreen!",1
  SetFontSize 16
  Text 320,360,"Press Fire to Play",1
  
  If GamePad(ButtonA)
    Gosub StartGame
    GameRun=1
  Endif
Return



// This resets the current level
.NextLevel
  DropDown=480
  For BadGuy=1 to 20
    EnemyAlive(BadGuy)=1
  Next

Return


// This prepares our game when the A Button is hit
.StartGame
  Score=0
  PlayerX=320 // Middle of the screen
  PlayerY=440 // Bottom of the screen
  
  BulletX=0  // Reset player's bullet
  BulletY=0
  
  Gosub NextLevel
Return



// This is our ingame loop
.InGame
CLS
ResetDraw

  // Starfield using Symbol 4
  Starfield 1,0,4

  // Player
  DrawImg PlayerX,PlayerY,2
  Speed=4
  If GamePad(ButtonLeft)>0.5 then PlayerX=PlayerX-Speed
  If GamePad(ButtonRight)>0.5 then PlayerX=PlayerX+Speed
  // Using >0.5 allows players to use a thumbstick, too!
  
  PlayerX=Limit(PlayerX,32,640-32)
  
  
  // Player's Bullet
  // Keep the Bullet going upwards
  BulletSpeed=16
  BulletY=Limit(BulletY-BulletSpeed, -100,480)

  // Draw the Bullet
  SetScale 0.2,0.5

  BulletCollide=DrawImg(BulletX,BulletY,3)

  // And reset things afterwards
  ResetDraw
  
  If BulletY<0 And GamePad(ButtonA)
    BulletX=PlayerX
    BulletY=PlayerY
    PlaySFX("Lazer_2")
  EndIf

  
  // Enemy Section
  
  // Reuse this to keep the invaders animated
  ShowInvader=1
  if Wrap(Frames,0,20)<10 then ShowInvader=0
  
  CircleSize=160
  // New Variable for Circle Position
  CircleX=320+Sin(Frames)*160
  CircleY=200-DropDown
  DropDown=Limit( DropDown-5 ,0,480)
    // Why not try changing this value, too.
      // The higher the number the faster the drop.
      // Make it slower to have the invaders creep into view!
  
  Gap=15

  BaddyCount=0
  // A loop for 20 Bad Guys
  For BadGuy=1 to 20
    // If Alive
    If EnemyAlive(BadGuy)==1
    
      BaddyCount=BaddyCount+1

      // Each enemy should be further around the loop.
      PlusAngle=BadGuy*Gap
      Angle=Wrap(Frames + PlusAngle,0,360)
      EnemyX=CircleX+Cos(Angle)*CircleSize
      EnemyY=CircleY+Sin(Angle)*CircleSize
      EnemyColour=Wrap(BadGuy,0,7)
      
      // New collision code
      EnemyCollide=DrawImg(EnemyX,EnemyY,ShowInvader,EnemyColour)
      If Collide(BulletCollide,EnemyCollide)
        // If the bullet hits the Enemy

        // Turn off the Enemy
        EnemyAlive(BadGuy)=0
        // Make a Bang sound
        PlaySFX("Explode_Low_2")  
        // Gimme 5 points!
        Gimme 5,EnemyX,EnemyY      
        
        // Move the Bullet off the screen
        BulletY=-100
      EndIf // Collision
      
    EndIf // Alive
    
  Next // Each BadGuy

  If BaddyCount<1 then Gosub NextLevel

  
  // Quit button (Return)
  If GamePad(ButtonStart) then GameRun=0
    // We can hit the Return key to quit
    
  SetFontSize 16
  Text 320,16,Score,1
Return

Making Waves

OK, let's play with the waves for each round.

Back in Chapter 20's "Why Not Try" section, we peeked at what happens if we multiply the Angle inside our Sin and Cos commands.

Different multiples create different shapes.

Different multiples create different shapes.

We can take this concept and make it part of the .NextLevel subroutine.

  // New variables to multiply the EnemyX and Y Angles
  WaveX=Rand(1,3)
  WaveY=Rand(1,3)

This will give us a random wave amount every time a new level starts, which we can multiply with the Angle inside our main enemy loop.

  EnemyX=CircleX+Cos(Angle*WaveX)*CircleSize
  EnemyY=CircleY+Sin(Angle*WaveY)*CircleSize
If you try our game now, you'll see that each level has a slightly different attack pattern for our invading fleet.

Tighter Waves

We can also change the "Gap" value, too. This is the value that spaces out the enemies by multiplying the Angle for each BadGuy.

The gap is the space between each enemy.

The gap is the space between each enemy.

Currently Gap is constantly set to 15 inside the main loop. If we make this a random number, each wave will end up even more varied.

Take out the line Gap=15 from the main loop, and instead, we'll define a random gap inside .NextLevel to go with the WaveX and WaveY variables.

  Gap=Rand(3,15)
A more varied pattern.

A more varied pattern.

Now we're getting tighter!

Our game, so far.

// My Game
// by Mr Green
// Created 2025/12/16

Symbol 0,"2__0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0P?0_0/P?0!P0,P0_0/P0,P0?P,00P0_0/P00P,0.P,00P0_0/P00P,0.P,00P0_0/P00P,0.P,00P!0!P!00P,0_0?P@0_0_0_0_0_0/P!0.P!0_0_0_0_0_0.P?0PP0P?0_0_0!PP0_0_0/P.0PP0P.0_0_0/PP0_0_0_0,PP0_0@P_P,0PP0P_P,0.P0_0_0@P0.P0_0_0@P0.P0_0_0@P0.P0@P0@P0@P0.PP0/P0@P0/PP0?P0/P0P!0P0/P0!P?0,P0@P0,P?0@P,0,P0@P0,P,0_0?P,0@P,";
Symbol 1,"2__0_0_0_0_0_0_0_0_0_0_0_0_0_0_00P,0_0_0@P_P,0_0?P_P,0?P0_0?P0?P,0@P0_0?P0_0/P0_0?P0_0/P?0!P?0_0_00P0!P0_0_0!P@0_0_0_0_0_0/P!0.P!0_0_0_0_0_0.P?0PP0P?0_0_0!PP0_0_0/P.0PP0P.0_0_0/PP0_0_0_0,PP0_0!P_P?0PP0P_P!0_0_0_0,PP0_0_0_0,PP0_0_0_0,PP0_0P0@P0_0PP0_0P0@P0_0PP0_0P0P!0P0_0PP0_0P0P0.P0P0_0PP0_0P0P0.P0P0_0P,0/P,0P!0P,0/P.0_0_0_0P.0_0_0_0PP";
Symbol 2,"2__0_0_0_0_0_0_0_0_0_0_0_0_0NN00NN0_0_0/NPQNNQPN0_0_0!NPQ.PN0_0_0!NPQ.PN0_0_0!NPQ.PN0_0_0!NPQ.PN0_0_0!NPQ.PN0_0_0!NPQ.PN0_0_0!NPQ.PN0_0_0!NPQ.PN0_0_0!NPQ.PN0_0_0!NPQ.PN0_0_0?NPPQ.PPN0_0_0.NPPQ.PPN0_0_0.NPPQ.PPN0_0_0.NPPQ.PPN0_0_0.NPQ!PN0_0_0.NPQ!PN0_0_0.NPQ!PN0_0_0,NPPQ!PPN0_0_00NPPQ!PPN0_0_00NPPQ!PPN0_0_00NPPQ!PPN0_0,P_P_P_P0,PS_S_S_SP0PS_S_S_S,PPSSR,S,R,S,RRS,R,S,R,SSP0S_S_S_S,0,S_S_S_S";
Symbol 3,"2__P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P_P/";
Symbol 4,"2__0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_0_z0_0_0_0020,20_0_0_00;0_0_0_0z0;1;0z0_0_0_0;0_0_0_0020,20_0_0_00z";

AntiAlias Off // Turn off AntiAlias, for Crispy Invaders


// Arrays
Dim EnemyAlive(20)

// Main Loop
GameRun=0
Repeat
  If GameRun==0 then Gosub .StartScreen
  If GameRun==1 then Gosub .InGame
Flip
Forever


// This is our Titlescreen
.StartScreen
CLS
ResetDraw

  ShowInvader=1
  If Wrap(Frames,0,20)<10 Then ShowInvader=0
  DrawImg 320,160,ShowInvader

  SetFontSize 32
  Text 320,240,"Titlescreen!",1
  SetFontSize 16
  Text 320,360,"Press Fire to Play",1
  
  If GamePad(ButtonA)
    Gosub StartGame
    GameRun=1
  Endif
Return



// This resets the current level
.NextLevel
  DropDown=480
  For BadGuy=1 to 20
    EnemyAlive(BadGuy)=1
  Next

  // New variables to multiply the EnemyX and Y Angles
  WaveX=Rand(1,3)
  WaveY=Rand(1,3)
  Gap=Rand(3,15)
  
Return


// This prepares our game when the A Button is hit
.StartGame
  Score=0
  PlayerX=320 // Middle of the screen
  PlayerY=440 // Bottom of the screen
  
  BulletX=0  // Reset player's bullet
  BulletY=0
  
  Gosub NextLevel
Return



// This is our ingame loop
.InGame
CLS
ResetDraw

  // Starfield using Symbol 4
  Starfield 1,0,4

  // Player
  DrawImg PlayerX,PlayerY,2
  Speed=4
  If GamePad(ButtonLeft)>0.5 then PlayerX=PlayerX-Speed
  If GamePad(ButtonRight)>0.5 then PlayerX=PlayerX+Speed
  // Using >0.5 allows players to use a thumbstick, too!
  
  PlayerX=Limit(PlayerX,32,640-32)
  
  
  // Player's Bullet
  // Keep the Bullet going upwards
  BulletSpeed=16
  BulletY=Limit(BulletY-BulletSpeed, -100,480)

  // Draw the Bullet
  SetScale 0.2,0.5

  BulletCollide=DrawImg(BulletX,BulletY,3)

  // And reset things afterwards
  ResetDraw
  
  If BulletY<0 And GamePad(ButtonA)
    BulletX=PlayerX
    BulletY=PlayerY
    PlaySFX("Lazer_2")
  EndIf

  
  // Enemy Section
  
  // Reuse this to keep the invaders animated
  ShowInvader=1
  if Wrap(Frames,0,20)<10 then ShowInvader=0
  
  CircleSize=160
  // New Variable for Circle Position
  CircleX=320+Sin(Frames)*160
  CircleY=200-DropDown
  DropDown=Limit( DropDown-5 ,0,480)
    // Why not try changing this value, too.
      // The higher the number the faster the drop.
      // Make it slower to have the invaders creep into view!
  


  BaddyCount=0
  // A loop for 20 Bad Guys
  For BadGuy=1 to 20
    // If Alive
    If EnemyAlive(BadGuy)==1
    
      BaddyCount=BaddyCount+1

      // Each enemy should be further around the loop.
      PlusAngle=BadGuy*Gap
      Angle=Wrap(Frames + PlusAngle,0,360)
      EnemyX=CircleX+Cos(Angle*WaveX)*CircleSize
      EnemyY=CircleY+Sin(Angle*WaveY)*CircleSize
      EnemyColour=Wrap(BadGuy,0,7)
      
      // New collision code
      EnemyCollide=DrawImg(EnemyX,EnemyY,ShowInvader,EnemyColour)
      If Collide(BulletCollide,EnemyCollide)
        // If the bullet hits the Enemy

        // Turn off the Enemy
        EnemyAlive(BadGuy)=0
        // Make a Bang sound
        PlaySFX("Explode_Low_2")  
        // Gimme 5 points!
        Gimme 5,EnemyX,EnemyY      
        
        // Move the Bullet off the screen
        BulletY=-100
      EndIf // Collision
      
    EndIf // Alive
    
  Next // Each BadGuy

  If BaddyCount<1 then Gosub NextLevel

  
  // Quit button (Return)
  If GamePad(ButtonStart) then GameRun=0
    // We can hit the Return key to quit
    
  SetFontSize 16
  Text 320,16,Score,1
Return
1

Experiment with Values.

When we set the WaveX and WaveY values, we used very simple Rand(1,3) random values.

Theses are whole numbers, so either 1, 2 or 3.

These don't have to be between 1 and 3, and they don't have to be whole numbers, either.
Why not try changing these to use Rnd() instead.
Experiment with different numbers in the Rnd, too.
See how different the game looks with each different change.

 


Our game is coming together really quite well.

Sure would be nice of the baddies shot back at us, though.

Come back for the next chapter and we'll have a play with that!