Page 1 of 1

Lots of balls

Posted: Wed Feb 02, 2011 9:17 am
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

Re: Lots of balls

Posted: Wed Feb 02, 2011 10:25 am
by eesau

Code: Select all

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

Seriously though, nice one, thanks for posting!

Re: Lots of balls

Posted: Wed Feb 02, 2011 11:41 am
by Kwai chang caine
Splendid :shock:

Really great Kelebrindae works 8)

Thanks a lot for sharing

Re: Lots of balls

Posted: Wed Feb 02, 2011 5:25 pm
by Vitor_BossĀ®
Very nice code.

I found one little bug.
Image

Re: Lots of balls

Posted: Wed Feb 02, 2011 5:39 pm
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)

Re: Lots of balls

Posted: Thu Feb 03, 2011 7:29 am
by kenmo
Nice intuitive demo. I missed your Verlet example, I should go play with that now!

Re: Lots of balls

Posted: Thu Feb 03, 2011 1:25 pm
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...

Re: Lots of balls

Posted: Thu Feb 03, 2011 3:25 pm
by IceSoft
Maybe I sould make this example with the Chipmunk4PB wrapper too ;-)

Re: Lots of balls

Posted: Thu Feb 03, 2011 4:41 pm
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:

Re: Lots of balls

Posted: Sat Feb 05, 2011 5:18 pm
by Zach
This thread is absolutely shameless :twisted: