// 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
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!
"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.

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
So far, we've..
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!
// 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
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!
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!
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!
// 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
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.
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
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.
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.
// 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
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.
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!