Lots of balls

Advanced game related topics
Kelebrindae
Enthusiast
Enthusiast
Posts: 151
Joined: Tue Apr 01, 2008 3:23 pm

Lots of balls

Post by Kelebrindae »

(... which can be, I guess, quite an awkward title. :P )

Here's a little thing that I coded a few months ago, then forgot to post it here (it was before my experimentation with Verlet Physics http://www.purebasic.fr/english/viewtop ... 16&t=44665).
There's a lot of problems with this code, and I won't push it any further. So feel free to change/fix anything you want! :wink:

Code: Select all

; Author: Kelebrindae
; Date: october, 7, 2010
; PB version: v4.51
; OS: Windows

; --------------------------------------------------------------------------------------------
; Description:
; --------------------------------------------------------------------------------------------
; Quick and ugly (and very inaccurate) simulation of a bouncing balls. Pachinko, anyone?
;
; Controls:
; - [Space] : reset balls position
; - [Return]: draw a new "level"
; -    +    ; restart with bigger balls
; -    -    ; restart with smaller balls
; - Esc : quit
; 
; NB: Don't forget to turn off the debugger !
; --------------------------------------------------------------------------------------------

#SCREENWIDTH = 800
#SCREENHEIGHT = 500
#BLANKCOLOR = $010101

; These values applies to a 60Hz frame rate
#GRAVITY = 0.16
#STICKYNESS = 0.4
#STOPTHRESHOLD = 0.05

Structure ball_struct
  x.f
  y.f
  radius.i
  elasticity.f
  mass.f
  
  onTheGround.b
  
  vx.f  ; horizontal velocity
  vy.f  ; vertical velocity
EndStructure
Global NewList ball.ball_struct()

Global *ptrSilhouette.b
Global angle.i, nbStep.i,i.i
Global ballRadius.i = 12
Global collisionX.f,collisionY.f,normalX.f,normalY.f,VdotN.f,normalForceX.f,normalForceY.f
Global absVx.f, absVy.f,vxStep.f,vyStep.f
Global *ptrBall.ball_struct

; Store sinus and cosinus (faster)
Global Dim cosTable.f(360)
Global Dim sinTable.f(360)
For i=0 To 359
  cosTable(i) = Cos( Radian(i) )
  sinTable(i) = Sin( Radian(i) )
Next i
   
EnableExplicit

;- --- Procedures ---

; Draw a ball and grab it as a 3d Sprite
Procedure createBallSprite(radius.i)
  If IsSprite(0)
    FreeSprite(0)
  EndIf
  
  CreateImage(0,51,51)
  StartDrawing(ImageOutput(0))
    DrawingMode(#PB_2DDrawing_Gradient)
    FrontColor($BBBB00)
    BackColor($FFFF00)
    CircularGradient(14,14,25)
    Circle(25,25,25)
  StopDrawing()
  ResizeImage(0,radius*2,radius*2)  
  SaveImage(0,"ball.bmp")
  FreeImage(0)
  LoadSprite(0, "ball.bmp", #PB_Sprite_Texture)
  DeleteFile("ball.bmp")
  CreateSprite3D(0, 0)
EndProcedure

; Initialize (or reinitialize) the balls' positions
; According to the balls' size, create as much balls as possible at the top of the screen
Procedure initializeBalls(radius.i)
  Protected i.i,j.i
  
  i=10+ballRadius
  j=ballRadius
  
  ClearList(ball())
  Repeat
    AddElement(ball())
    ball()\x = i
    ball()\y = j
    ball()\radius = ballRadius
    ball()\mass = 10
    ball()\elasticity = 0.75
    ball()\vx = (20 - Random(40))/10
    ball()\vy = 0
    ball()\onTheGround = #False

    i + ballRadius*2 + 4
    If i > #SCREENWIDTH - 10 - ballRadius
      i=10+ballRadius
      j + ballRadius*2 + 4
    EndIf
  Until j > 50-ballRadius
EndProcedure

; Look up in the "silhouette" memory array if a given pixel is blank or not
Procedure.b checkCollision(xtest.i, ytest.i, levelWidth.i, levelHeight.i)
   
  If xtest < 1 Or ytest < 1 Or xtest > levelWidth-1 Or ytest > levelHeight-1
    ProcedureReturn 255
  EndIf
 
  ProcedureReturn PeekB(*ptrSilhouette + (ytest*levelWidth) + xtest)
   
EndProcedure


; Store the level's "silhouette" in memory to check collisions
Procedure storeSilhouette(numImage.i)
  Protected *ptrSilhouette,*ptr
  Protected i.i,j.i,width.i,height.i
 
  width = ImageWidth(numImage)
  height = ImageHeight(numImage)
  *ptrSilhouette = AllocateMemory(width * height)
 
  If *ptrSilhouette <> 0
    *ptr = *ptrSilhouette
     StartDrawing(ImageOutput(numImage))
    For j = 0 To height-1
      For i = 0 To width-1
        If Point(i,j) <> #BLANKCOLOR
          PokeB(*ptr,255)
        Else
          PokeB(*ptr,0)
        EndIf
        *ptr+1
      Next i
    Next j
    StopDrawing()
  EndIf
 
  ProcedureReturn *ptrSilhouette
EndProcedure

; Draw a random background And create a 3D sprite from it
Procedure drawLevel(*ptrSilhouette = 0)
  Protected i.i,x.i,y.i,w.i,h.i
  
  ; Free all previously used ressources
  If IsSprite(999)
    FreeSprite(999)
  EndIf
  If IsSprite3D(999)
    FreeSprite3D(999)
  EndIf
  If *ptrSilhouette <> 0
    FreeMemory(*ptrSilhouette)
  EndIf

  
  CreateImage(0,#SCREENWIDTH, #SCREENHEIGHT)
  StartDrawing(ImageOutput(0)) 
    Box(0,0,#SCREENWIDTH,#SCREENHEIGHT,#BLANKCOLOR)
    DrawingMode(#PB_2DDrawing_Gradient)
    FrontColor($616161)
    BackColor($EAEAEA)
    For i = 0 To 20
      x=Random(#SCREENWIDTH):y=50+Random(#SCREENHEIGHT-50):w=Random(25)+10:h=Random(25)+10
      LinearGradient(x,y,x+h/2,y+h)
      Box(x,y,w,h)
    Next i
    FrontColor($77777A)
    BackColor($CCCCCD)
    For i = 0 To 20
      x=Random(#SCREENWIDTH):y=50+Random(#SCREENHEIGHT-50):w=Random(25)+10
      CircularGradient(x-(w/2),y-(w/2),w)
      Circle(x,y,w)
    Next i 
    
    DrawingMode(#PB_2DDrawing_Default)
    Box(0,0,#SCREENWIDTH,50,#BLANKCOLOR)
    DrawingMode(#PB_2DDrawing_Outlined)
    Box(0,0,#SCREENWIDTH,#SCREENHEIGHT,$FFFFFF)
  StopDrawing()
    
  SaveImage(0,"decor.bmp")
  LoadSprite(999, "decor.bmp", #PB_Sprite_Texture)
  DeleteFile("decor.bmp")
  CreateSprite3D(999,999)
  
  ; Store level's "silhouette" in memory for faster collision check
  ; (it uses a lot of memory, though)
  *ptrSilhouette = storeSilhouette(0)
  FreeImage(0)
  
  ; Return the pointer to the silhouette
  ProcedureReturn *ptrSilhouette 
EndProcedure

; Check collision between two balls. If they collide, set their new speed and direction
; (conversion of a Blitz Basic code from Jim Brown, here: http://www.blitzbasic.com/Community/posts.php?topic=55823 )
Procedure collideBalls(*ptrBall1.ball_struct,*ptrBall2.ball_struct)
  Protected collisionDistance.f,actualDistance.f
  Protected realcollNormalAngle.f,collNormalAngle.i
  Protected moveDist1.f,moveDist2.f
  Protected nX.f,nY.f,a1.f,a2.f,optimisedP.f
  
  ; Check distance between the balls (don't use SQR to make it faster)
  collisionDistance = (*ptrBall1\radius + *ptrBall2\radius)*(*ptrBall1\radius + *ptrBall2\radius)
  actualDistance = ( (*ptrBall2\x - *ptrBall1\x)*(*ptrBall2\x - *ptrBall1\x) + (*ptrBall2\y - *ptrBall1\y)*(*ptrBall2\y - *ptrBall1\y) )
  
  ; If the balls collide
  If actualDistance < collisionDistance
    ; well, now we need real distances, so => SQR
    collisionDistance = *ptrBall1\radius + *ptrBall2\radius
    actualDistance = Sqr(actualDistance) 

    ; Compute angle between the to ball    
    realcollNormalAngle = Degree(ATan2(*ptrBall2\x - *ptrBall1\x,*ptrBall2\y - *ptrBall1\y ))
    
    ; Convert this angle to an integer in the 0 - 359 range (to use in the Cos/Sin tables)
    If realcollNormalAngle < 0
      collNormalAngle = realcollNormalAngle + 360
    Else
      collNormalAngle = realcollNormalAngle 
    EndIf  
    
    ; Move the balls so they don't intersect, according to the collision angle and their mass
    moveDist1=(collisionDistance-actualDistance) * (*ptrBall2\mass / (*ptrBall1\mass + *ptrBall2\mass))
    moveDist2=(collisionDistance-actualDistance) * (*ptrBall1\mass / (*ptrBall1\mass + *ptrBall2\mass))
    *ptrBall1\x - moveDist1 * cosTable(collNormalAngle)
    *ptrBall1\y - moveDist1 * sinTable(collNormalAngle)
    *ptrBall2\x + moveDist2 * cosTable(collNormalAngle)
    *ptrBall2\y + moveDist2 * sinTable(collNormalAngle)
        
    ; COLLISION RESPONSE
    ; n = vector connecting the centers of the balls.
    ; nX and nY are the components of the normalised vector n
    nX = cosTable(collNormalAngle)
    nY = sinTable(collNormalAngle)
    
    ; Find the components of each movement vectors along n, by using dot product
    a1 = *ptrBall1\vx * nX + *ptrBall1\vy * nY
    a2 = *ptrBall2\vx * nX + *ptrBall2\vy * nY
    
    ; optimisedP = 2(a1 - a2)
    ;             ----------
    ;              m1 + m2
    optimisedP = (2.0 * (a1-a2)) / (*ptrBall1\mass + *ptrBall2\mass)
    
    ; Resultant vector for first ball
    *ptrBall1\vx - (optimisedP * *ptrBall2\mass * nX)
    *ptrBall1\vy - (optimisedP * *ptrBall2\mass * nY)
    
    ; Resultant vector for second ball
    *ptrBall2\vx + (optimisedP * *ptrBall1\mass * nX)
    *ptrBall2\vy + (optimisedP * *ptrBall1\mass * nY)
  EndIf
  
EndProcedure


;- --- Main program ---
InitSprite()
InitSprite3D()
InitKeyboard()

;- Window
OpenWindow(0, 0, 0, #SCREENWIDTH, #SCREENHEIGHT, "Bounce", #PB_Window_ScreenCentered|#PB_Window_SystemMenu)
OpenWindowedScreen(WindowID(0), 0, 0, #SCREENWIDTH,#SCREENHEIGHT, 0, 0, 0,#PB_Screen_SmartSynchronization)

;- Create a sprite for the balls
createBallSprite(ballRadius)

;- Create a random level
*ptrSilhouette = drawLevel()

;- Create balls
initializeBalls(ballRadius)

;- --- Main Loop ---
Repeat
  While WindowEvent() : Wend
  
  ForEach ball()
    ; If the ball is moving or falling, check for collision
    If ball()\vx <> 0 Or ball()\vy <> 0 Or ball()\onTheGround = #False
      
      ;- Collision with the background
      ; If X-velocity or Y-velocity exceeds 1, break movement into N steps where "partial" velocities are smaller then 1 (= sub-sampling)
      If ball()\Vx > 1 Or ball()\Vx < -1 Or ball()\Vy > 1 Or ball()\Vy < -1
        absVx = Abs(ball()\Vx)
        absVy = Abs(ball()\Vy)
        If absVx > absVy
          nbStep = Int(absVx) + 1
        Else
          nbStep = Int(absVy) + 1
        EndIf
        vxStep = ball()\Vx / nbStep
        vyStep = ball()\Vy / nbStep
      Else
        nbStep = 1
        vxStep = ball()\Vx
        vyStep = ball()\Vy
      EndIf
     
      ; For each step, check for collision and move the ball
      For i = 1 To nbStep
    
        ;Check 12 collision points around the ball
        For angle = 0 To 330 Step 30
          ; Get coords for check point at angle [angle]
          collisionX = cosTable(angle)
          collisionY = sinTable(angle)
            
          ; Check collision
          If checkCollision( ball()\x + collisionX * ball()\radius, ball()\y + collisionY * ball()\radius ,#SCREENWIDTH,#SCREENHEIGHT)
            ; Get normal to collision
            normalX = -collisionX
            normalY = -collisionY
                    
            ; Move ball a bit away from collision
            ball()\y + normalY
            ball()\x + normalX
            
            ; Project velocity onto collision normal vector
            VdotN = vxStep * normalX + vyStep * normalY
            
            ; Calculate normal force
            normalForceX = -2.0 * normalX * VdotN
            normalForceY = -2.0 * normalY * VdotN
              
            ; Add normal force to velocity vector
            vxStep + normalForceX * ball()\elasticity
            vyStep + normalForceY * ball()\elasticity
          EndIf
          
        Next angle
         
        ; Move the ball   
        ball()\x + vxStep
        ball()\y + vyStep
      Next i
     
      ; Total velocities =  partial velocities * number of steps
      ball()\vx = vxStep * nbStep
      ball()\vy = vyStep * nbStep
       
      ; If the ball is in the air
      If checkCollision( ball()\x, ball()\y + ball()\radius + 1,#SCREENWIDTH,#SCREENHEIGHT) = 0
        ; Apply gravity
        ball()\vy + #GRAVITY
        ball()\onTheGround = #False
      Else
        ; If the ball is on the ground and its falling speed is under the "stickyness" threshold, consider it's rolling 
        If ball()\vy < #STICKYNESS And ball()\vy > 0        
          ball()\onTheGround = #True
          ball()\vy = 0
        EndIf
        
        ; If the ball is rolling on the ground, apply friction
        If ball()\onTheGround = #True
          ball()\vx * 0.99
          
          ; if the ball rolling speed is sufficiently small, stop it.
          If ball()\vx < #STOPTHRESHOLD And ball()\vx > -#STOPTHRESHOLD 
            ball()\vx = 0
          EndIf
          
        EndIf ; if ball()\onTheGround = #True...
      EndIf ; else... (ball is touching the ground)
      
      ;- Collision with all the other balls
      *ptrBall = @ball()
      ForEach ball()
        If @ball() <> *ptrBall
          collideBalls(*ptrBall,@ball())
        EndIf
      Next ball()
      ChangeCurrentElement(ball(),*ptrBall)
      
    EndIf ; if the ball is moving or falling...
  Next ball()  
    
  ;- Display screen
  Start3D()
    ; Background   
    DisplaySprite3D(999,0,0,255)      
    ; Balls
    ForEach ball()
      DisplaySprite3D(0,ball()\x-ball()\radius,ball()\y-ball()\radius)
    Next ball()
  Stop3D()   
     
  FlipBuffers()
  
  ;- Keyboard
  ExamineKeyboard()
  
  ; Space => reset sim
  If KeyboardReleased(#PB_Key_Space)
    initializeBalls(ballRadius)
  EndIf
  ; Return => draw a new "level"
  If KeyboardReleased(#PB_Key_Return)
    *ptrSilhouette = drawLevel(*ptrSilhouette )
    initializeBalls(ballRadius)
  EndIf
  
  
  ; minus => reset sim with smaller balls
  If (KeyboardReleased(#PB_Key_Minus) Or KeyboardReleased(#PB_Key_Subtract)) And ballRadius > 2
    ballRadius - 2
    createBallSprite(ballRadius)
    initializeBalls(ballRadius)
  EndIf
  ; plus => reset sim with bigger balls
  If (KeyboardReleased(#PB_Key_Equals) Or KeyboardReleased(#PB_Key_Add)) And ballRadius < 20
    ballRadius + 2
    createBallSprite(ballRadius)
    initializeBalls(ballRadius)
  EndIf

Until KeyboardPushed(#PB_Key_Escape)

FreeMemory(*ptrSilhouette)
End
eesau
Enthusiast
Enthusiast
Posts: 589
Joined: Fri Apr 27, 2007 12:38 pm
Location: Finland

Re: Lots of balls

Post by eesau »

Code: Select all

; -    +    ; restart with bigger balls
; -    -    ; restart with smaller balls
:lol:

Seriously though, nice one, thanks for posting!
User avatar
Kwai chang caine
Always Here
Always Here
Posts: 5522
Joined: Sun Nov 05, 2006 11:42 pm
Location: Lyon - France

Re: Lots of balls

Post by Kwai chang caine »

Splendid :shock:

Really great Kelebrindae works 8)

Thanks a lot for sharing
ImageThe happiness is a road...
Not a destination
Vitor_Boss®
User
User
Posts: 81
Joined: Thu Sep 23, 2010 4:22 am

Re: Lots of balls

Post by Vitor_Boss® »

Very nice code.

I found one little bug.
Image
Sorry by bad English.
HP Pavilion DV6-2155DX: Intel i3-330m 2.13 / 4GB DDR3 / 500GB Sata2 HD / Display 15.6" LED / Win7 Ultimate x64 / PB 4.50 x86 demo.
User avatar
Rook Zimbabwe
Addict
Addict
Posts: 4322
Joined: Tue Jan 02, 2007 8:16 pm
Location: Cypress TX
Contact:

Re: Lots of balls

Post by Rook Zimbabwe »

You got balls for posting this here... :wink:

Fantastic example... now if I can integrate with verlet and Idles attractors and repulsors... hmmm 8)
Binarily speaking... it takes 10 to Tango!!!

Image
http://www.bluemesapc.com/
User avatar
kenmo
Addict
Addict
Posts: 2069
Joined: Tue Dec 23, 2003 3:54 am

Re: Lots of balls

Post by kenmo »

Nice intuitive demo. I missed your Verlet example, I should go play with that now!
Kelebrindae
Enthusiast
Enthusiast
Posts: 151
Joined: Tue Apr 01, 2008 3:23 pm

Re: Lots of balls

Post by Kelebrindae »

@Kcc and all the others:
Thanks for the nice comments!

@eesau & Rook Zimbabwe:
yes, I was painfully aware of all the potential innuendos. That was part of the fun of it... :wink:

@Vitor_Boss:
This is a known issue: when your balls are too tightly packed against each other (and this is particulary true for smallest ball sizes), they begin to bang around chaotically and finally pass through each others or through the nearest obstacle; and I certainly wouldn't want that to happen. :mrgreen:
But as I said in my first post, there's a lot of bugs and imperfections in this code, and I don't want to fix them all...
User avatar
IceSoft
Addict
Addict
Posts: 1699
Joined: Thu Jun 24, 2004 8:51 am
Location: Germany

Re: Lots of balls

Post by IceSoft »

Maybe I sould make this example with the Chipmunk4PB wrapper too ;-)
Belive! C++ version of Puzzle of Mystralia
Bug Planet
<Wrapper>4PB, PB<game>, =QONK=, PetriDish, Movie2Image, PictureManager,...
User avatar
Rook Zimbabwe
Addict
Addict
Posts: 4322
Joined: Tue Jan 02, 2007 8:16 pm
Location: Cypress TX
Contact:

Re: Lots of balls

Post by Rook Zimbabwe »

This is a known issue: when your balls are too tightly packed against each other
Switch to Boxers and buy the next larger sized underwear and this will not happen! :wink:

OK I shall stop! :mrgreen:
Binarily speaking... it takes 10 to Tango!!!

Image
http://www.bluemesapc.com/
Zach
Addict
Addict
Posts: 1678
Joined: Sun Dec 12, 2010 12:36 am
Location: Somewhere in the midwest
Contact:

Re: Lots of balls

Post by Zach »

This thread is absolutely shameless :twisted:
Post Reply