Sunday, 15 April 2018

PureBasic Parallax Starfield

I've always wanted to create a parallax starfield since the days of my C64 game-playing and demo-watching youth, but never had the tools or skills to create one.  Time has come...

After coding a couple of applications in PureBasic, I decided to turn my attention to the graphical abilities of said programming language. Research time and first port of call was the PureBasic forum where I found a few examples, but they were either too old for my version of PureBasic so wouldn't compile, or were overly complicated with lots of added effects that I didn't want or need.

So I decided to go it alone and code a clean and simple routine that I would then be able to 'plug' into other projects if I needed to fill the background with something interesting, as I did with 'Old School Demo 1'.  This is what I came up with to generate a simple 4 layer starfield...

       
;- INITIALISE ENVIRONMENT ------------------------------------

InitKeyboard()
InitSprite()


;- CREATE VARIABLES ------------------------------------------

#XRES = 800
#YRES = 600
#LAYER1SPEED = 1
#LAYER2SPEED = 3
#LAYER3SPEED = 5
#LAYER4SPEED = 10


;- PROCEDURES ------------------------------------------------

Procedure OpenMainWindow()
  
  OpenWindow(0,0,0,#XRES,#YRES,"Parallax Stars!",#PB_Window_ScreenCentered)
  OpenWindowedScreen(WindowID(0),0,0,#XRES,#YRES)
  
EndProcedure

Procedure CreateStarfields()
  
  Global sp_BKGRDSTAR = CreateSprite(#PB_Any,#XRES,#YRES)
  StartDrawing(SpriteOutput(sp_BKGRDSTAR))
  For n=0 To 999
    Plot(Random(#XRES-1),Random(#YRES-1),RGB(100,100,100))
  Next
  StopDrawing()
  
  Global sp_MDGRDSTAR = CreateSprite(#PB_Any,#XRES,#YRES)
  StartDrawing(SpriteOutput(sp_MDGRDSTAR))
  For n=0 To 299
    Plot(Random(#XRES-1),Random(#YRES-1),RGB(150,150,150))
  Next
  StopDrawing()
  
  Global sp_FTGRDSTAR = CreateSprite(#PB_Any,#XRES,#YRES)
  StartDrawing(SpriteOutput(sp_FTGRDSTAR))
  For n=0 To 199
    Plot(Random(#XRES-1),Random(#YRES-1),RGB(200,200,200))
  Next
  StopDrawing()
  
  Global sp_CLGRDSTAR = CreateSprite(#PB_Any,#XRES,#YRES)
  StartDrawing(SpriteOutput(sp_CLGRDSTAR))
  For n=0 To 19
    Plot(Random(#XRES-1),Random(#YRES-1),RGB(255,255,255))
  Next
  StopDrawing() 
    
EndProcedure

Procedure DrawStarfields()
  
  Shared bgx1, bgx2, bgx3, bgx4
  
  For s1 = 0 To 1
    For s2 = 0 To 1
      DisplayTransparentSprite(sp_BKGRDSTAR,#XRES*s1-bgx1,#YRES*s2)
      DisplayTransparentSprite(sp_MDGRDSTAR,#XRES*s1-bgx2,#YRES*s2)
      DisplayTransparentSprite(sp_FTGRDSTAR,#XRES*s1-bgx3,#YRES*s2)
      DisplayTransparentSprite(sp_CLGRDSTAR,#XRES*s1-bgx4,#YRES*s2)
    Next
  Next
  
  bgx1+#LAYER1SPEED : If bgx1 > #XRES-1 : bgx1=0 : EndIf
  bgx2+#LAYER2SPEED : If bgx2 > #XRES-1 : bgx2=0 : EndIf
  bgx3+#LAYER3SPEED : If bgx3 > #XRES-1 : bgx3=0 : EndIf
  bgx4+#LAYER4SPEED : If bgx4 > #XRES-1 : bgx4=0 : EndIf
  
EndProcedure    


;- BEGIN PROGRAM ----------------------------------------------------       

OpenMainWindow()                            
CreateStarfields()
HideWindow(0,#False)


;- MAIN PROGRAM LOOP ------------------------------------------------  

Repeat
  
  WaitWindowEvent(1)
  ClearScreen(0)
  DrawStarfields()
  FlipBuffers()
  ExamineKeyboard()
  
Until KeyboardPushed(#PB_Key_Escape)


;- EXIT PROGRAM ----------------------------------------------------     

FreeSprite(#PB_All)
CloseWindow(#PB_All)

End

The above code will actually run stand alone; if you compile it in PureBasic, it will open its own window and display the scrolling starfield until you exit by pressing 'escape'.

What is each part of the code doing?

;- INITIALISE ENVIRONMENT ------------------------------------

InitKeyboard()
InitSprite()

This section does what the comment says and initialises the environment - to be able to detect keypresses you must initialise the keyboard and to be able to use any graphics (including sprites) in a window you must initialise the sprite system.

;- CREATE VARIABLES ------------------------------------------

#XRES = 800
#YRES = 600
#LAYER1SPEED = 1
#LAYER2SPEED = 3
#LAYER3SPEED = 5
#LAYER4SPEED = 10

This section creates variables and assigns number values.  Changing the value of #XRES and #YRES alters the size of the window in which the starfield is displayed.  The other variables created control the speed of the scroll of each star layer, in this example 'layer 1' is the slowest to simulate being the furthest away and layer 4 the quickest to simulate being the closest, with layers 2 and 3 being somewhere in between.

Next, I've created some procedures.

;- PROCEDURES ------------------------------------------------

Procedure OpenMainWindow()
  
  OpenWindow(0,0,0,#XRES,#YRES,"Parallax Stars!",#PB_Window_ScreenCentered)
  OpenWindowedScreen(WindowID(0),0,0,#XRES,#YRES)
  
EndProcedure

This first procedure opens a window on the users screen with the size of #XRES and #YRES, with a title "Parallax Stars!" and centres the window in the users display.  It then opens a 'windowed screen' in this window which enables the use of powerful graphic commands that display images and sprites really quickly, enough so to create fast demos and games.

Procedure CreateStarfields()
  
  Global sp_BKGRDSTAR = CreateSprite(#PB_Any,#XRES,#YRES)
  StartDrawing(SpriteOutput(sp_BKGRDSTAR))
  For n=0 To 999
    Plot(Random(#XRES-1),Random(#YRES-1),RGB(100,100,100))
  Next
  StopDrawing()
  
  Global sp_MDGRDSTAR = CreateSprite(#PB_Any,#XRES,#YRES)
  StartDrawing(SpriteOutput(sp_MDGRDSTAR))
  For n=0 To 299
    Plot(Random(#XRES-1),Random(#YRES-1),RGB(150,150,150))
  Next
  StopDrawing()
  
  Global sp_FTGRDSTAR = CreateSprite(#PB_Any,#XRES,#YRES)
  StartDrawing(SpriteOutput(sp_FTGRDSTAR))
  For n=0 To 199
    Plot(Random(#XRES-1),Random(#YRES-1),RGB(200,200,200))
  Next
  StopDrawing()
  
  Global sp_CLGRDSTAR = CreateSprite(#PB_Any,#XRES,#YRES)
  StartDrawing(SpriteOutput(sp_CLGRDSTAR))
  For n=0 To 19
    Plot(Random(#XRES-1),Random(#YRES-1),RGB(255,255,255))
  Next
  StopDrawing() 
    
EndProcedure

This next procedure then creates 4 sprites 'on the fly' and then randomly fills them with dots pretending to be stars.  Each sprite is held in a named variable starting with 'sp_' to indicate it's a sprite plus a name to indicate it's level in the display, so 'BKGRDSTAR' is the background layer, 'MDGRDSTAR' being the mid layer and so on.  This is my naming convention, you could call the sprite whatever you like.  Each sprite is created the same size as the window (#XRES, #YRES).

When randomly filling with dots, the closer the layer the fewer the number of dots (n). Also, since the background stars would be further away they are darker, RGB(100,100,100), while the closest layer is brighter, RGB(255,255,255).

Procedure DrawStarfields()
  
  Shared bgx1, bgx2, bgx3, bgx4
  
  For s1 = 0 To 1
    For s2 = 0 To 1
      DisplayTransparentSprite(sp_BKGRDSTAR,#XRES*s1-bgx1,#YRES*s2)
      DisplayTransparentSprite(sp_MDGRDSTAR,#XRES*s1-bgx2,#YRES*s2)
      DisplayTransparentSprite(sp_FTGRDSTAR,#XRES*s1-bgx3,#YRES*s2)
      DisplayTransparentSprite(sp_CLGRDSTAR,#XRES*s1-bgx4,#YRES*s2)
    Next
  Next
  
  bgx1+#LAYER1SPEED : If bgx1 > #XRES-1 : bgx1=0 : EndIf
  bgx2+#LAYER2SPEED : If bgx2 > #XRES-1 : bgx2=0 : EndIf
  bgx3+#LAYER3SPEED : If bgx3 > #XRES-1 : bgx3=0 : EndIf
  bgx4+#LAYER4SPEED : If bgx4 > #XRES-1 : bgx4=0 : EndIf
  
EndProcedure  

This next procedure draws the actual sprites (starfields) on the screen when this procedure is called later in the code.  The sprites are drawn in 'transparent' mode, whereby everything that is black is transparent so shows everything underneath.  Therefore, the stars (dots) on each sprite layer are always visible.

Each sprite layer is moved sideways using the #LAYERSPEED variables, with each layer moving a set number of pixels defined by that variable.  The sprites loop around continuously when they have scrolled their #XRES limit.

;- BEGIN PROGRAM ----------------------------------------------------       

OpenMainWindow()                            
CreateStarfields()
HideWindow(0,#False)

Now the program begins to actually execute by calling the 'OpenMainWindow()' procedure to open a window, generate the sprites by calling the 'CreateStarfields()' procedure and then unhides the window so we can see it, if it was hidden in the 'openwindow' procedure (which in this example it actually isn't).

;- MAIN PROGRAM LOOP ------------------------------------------------  

Repeat
  
  WaitWindowEvent(1)
  ClearScreen(0)
  DrawStarfields()
  FlipBuffers()
  ExamineKeyboard()
  
Until KeyboardPushed(#PB_Key_Escape)

The code now enter a continuous loop where it waits for a window event, clears the screen each frame, draws the starfields in their new positions, flips to the second prepared screen where the drawing of starfields has taken place out of view to ensure smoothness, then checks the keyboard to see if the user has pressed 'escape'.

;- EXIT PROGRAM ----------------------------------------------------     

FreeSprite(#PB_All)
CloseWindow(#PB_All)

End

If the user pressed space, then the program loop exits, erases all sprites and closes the window.  Technically, the 'End' command does this automatically but I like to be thorough.

This code can easily be adapted to make the stars scroll the opposite direction, or even up/down.

If you want the raw code to copy and paste into PureBasic, grab it from Pastebin here...

No comments:

Post a Comment