Friday, February 16, 2018

Simulation #1: 2D Pool game (part 2 - program)

In this article, I am going to describe program, that realizes 2D pool simulation.

Language that I chose is Python. I like it, because writing process is fast: it has concised syntax and many built in helpful methods. I am aware that it doesn't belong to fastest languages, but I think that it is completely enough fast to realize this task.

Program consists of 3 classes: Ball, Table and Simulation. Simulation class realize GUI besides running simulation. Below, I described in the form of table, content of each class:

Class: Ball
VariablesDescription
ttime
dttime step
x0,y0initial coordinates of the ball (t=0)
vx0,vy0initial velocity of the ball (t=0)
x,ycurrent ball coordinates
vx,vycurrent ball velocity
Rradius of the ball
idball number (id)
colorball color, for example "blue"
collisionBallNrid of ball that collids with current ball
ifCollisionCompletedvariable of Boolean type, returns True if collision with ball is completed
MethodsDescription
nextStepcalculates next ball position after dt (according to equation (0) from part 1)
ifCollisionWithBallcheck if collision with ball happened, if yes - it calculates new velocities for it
ifCollisionWithEdgecheck if collision with edge of the table happened, if yes - it calculates new velocities for it
setInitialBallVelocityset random initial velocity for the ball

Class: Table
VariablesDescription
L, Wtable dimentions
borderCoordinatesXarray contains 2 edge values of x variable - that are: 0 and L
borderCoordinatesYarray contains 2 edge values of y variable - that are: 0, W
Nnumber of balls
ballsarray that contains Ball objects
colorsarray contains name of colors
MethodsDescription
setInitialBallsPositionsset initial random positions for all balls, in a way that they do not overlap themself
actualPositionrun next step of simulation, i.e. run methods: nextStep, ifCollisionWithEdge, ifCollisionWithBall, returns actual position after the step
actualVelocityauxiliary method, returns actual velovity of selected ball
getInitialPositionsAndVelocitiesauxiliary method, returns initial velocities of all balls


Class: Simulation
VariablesDescription
ttime
ifSimulationWorksauxiliary variable of boolean type
ifPauseauxiliary variable of boolean type
MethodsDescription
getNDtgets N and dt variables from input fields
runSimulationruns simulation
stopstops simulation

The program window and the working simulation are finally presented as follows:
You can find project source and executable file on github: https://github.com/sim-num/2DpoolGameSimulation

Steps to program simulation:

  1. Set initial values for each ball (position and velocity)
  2. Calculate new position according to equation (0) from part 1
  3. Check if collision with another ball or with the edge of the table occures (if yes - calculate new velocity)
  4. Go back to step 2.


  • Ad 1. Velocity initialisation (in class Ball):
    
        def setInitialBallVelocity(self):
            self.vx0 = random.randint(-10, 10)
            self.vy0 = random.randint(-10, 10)
            self.vx = self.vx0
            self.vy = self.vy0
           
     
    Initial velocity vector components are initialised with random integer values from range (-10, 10). Initialisation of position vector is realised by following method (in class Table):
    
        def setInitialBallsPositions(self, balls):
    
            for i in balls:
                while i.x0 == -1 and i.y0 == -1:
                    x = random.randint(i.R, Table.L - i.R)
                    y = random.randint(i.R, Table.W - i.R)
                    for j in balls:
                        if i.id != j.id and x <= (j.x + 2 * j.R) and y <= (j.y + 2 * j.R) and x >= (
                                j.x - 2 * j.R) and y >= (j.y - 2 * j.R):
                            x = -1
                            y = -1
                    i.x0 = x
                    i.y0 = y
                    i.x = i.x0
                    i.y = i.y0
           
     
    To initialise ball position, it is need to know other balls positions to avoid overlapping. This is the reason why position is initialised in class Table instead of Ball. We search for not overlapped position until it is found (while loop is used for that reason).
  • Ad 2. Calculation of new position is performed by following method (in class Ball):
  • 
        def nextStep(self):
            self.x = self.x + self.vx * Ball.dt
            self.y = self.y + self.vy * Ball.dt
           
     
    As you can easily see, equation (0) is programmed inside method nextStep. Second method that calculate next position is method called actualPosition inside class Table. It just calls method nextStep as well as other methods related to collision handling (see point 3).
    
        def actualPosition(self, ballNr, t):
    
            for i in range(0, self.N):
                self.balls[i].nextStep()
                self.balls[i].ifCollisionWithEdge()
                self.balls[i].ifCollisionWithBall(self.balls)
            return [self.balls[ballNr].x, self.balls[ballNr].y]
           
     
    After ball move due to calling nextStep method, ifCollisionWithEdge and ifCollisionWithBall methods modify velocity vector appropriately if the collision conditions are met.
  • Ad 3. Methods ifCollisionWithEdge and ifCollisionWithBall as said before are responsible for collision handling. Both belongs to class Ball. First method ifCollisionWithEdge is responsible for collision handling with edge. According to theory (see case 1 in part 1), depending on whether ball collids with vertical or horizontal edge of the table - respectively $V_x$ or $V_y$ component changes:
  • 
        def ifCollisionWithEdge(self):
            if self.x + self.R >= Table.borderCoordinatesX[1] and self.vx > 0:
                self.vx = -self.vx
            if self.x - self.R <= Table.borderCoordinatesX[0] and self.vx < 0:
                self.vx = -self.vx
            if self.y - self.R <= Table.borderCoordinatesY[0] and self.vy < 0:
                self.vy = -self.vy
            if self.y + self.R >= Table.borderCoordinatesY[1] and self.vy > 0:
                self.vy = -self.vy
           
     
    ifCollisionWithBall is most complex method in program. Bellow is description step by step, how is it programmed (flag variable called ifCollisionCompleted is initialised in __init__ class method with value False):
    1. Calculate distance between ball and another ball (for loop over all balls)
    2. If distance is smaller than 2R and ifCollisionCompleted is False, then go to 3. else set ifCollisionCompleted to False for both balls and go back to step 1
    3. Calculate $\alpha$ angle. It is related to arctan function as shown in part 1.
    4. Calculate normal and tangent component of velocity vector for both collided balls (use equation (1b) from part 1.)
    5. Swap normal component of velocity vector between balls.
    6. Set ifCollisionCompleted flag to True for both balls.
    
        def ifCollisionWithBall(self, balls):  
            Xcoll = 0
            Ycoll = 0
            distanceFromBall = 99999
            for i in balls:
                if i.id != self.id:
                    distanceFromBall = sqrt((self.x - i.x) ** 2 + (self.y - i.y) ** 2)
                    if distanceFromBall <= 2 * self.R and self.ifCollisionCompleted == False:
                        self.collisionBallNr = i.id
                        Xcoll = (self.x + i.x) / 2
                        Ycoll = (self.y + i.y) / 2
                        alpha = 3.14159 / 2 - atan(float(self.y - i.y) / (self.x - i.x))  # collision angle
    
                        Vs = cos(alpha) * self.vx - sin(alpha) * self.vy
                        Vn = sin(alpha) * self.vx + cos(alpha) * self.vy
                        Vs_i = cos(alpha) * i.vx - sin(alpha) * i.vy
                        Vn_i = sin(alpha) * i.vx + cos(alpha) * i.vy
                        Vn_po = Vn_i
                        Vni_po = Vn
                        self.vx = cos(alpha) * Vs + Vn_po * sin(alpha)
                        self.vy = cos(alpha) * Vn_po - sin(alpha) * Vs
                        i.vx = cos(alpha) * Vs_i + Vni_po * sin(alpha)
                        i.vy = cos(alpha) * Vni_po - sin(alpha) * Vs_i
                        i.ifCollisionCompleted = True
                        self.ifCollisionCompleted = True
                    elif distanceFromBall > 2 * self.R and self.ifCollisionCompleted == True and i.id == self.collisionBallNr:
                        self.ifCollisionCompleted = False
                        i.ifCollisionCompleted = False
           
     
    Please notice that flag ifCollisionCompleted is static variable, which means that it is sufficient to set it by any of 2 collided balls. It should be like that, because if first ball ends the collision - second should know it.

Simulation visualisation


To visulize simulation I used Tkinter python library. Visualisation is implemented in method runSimulation, which belongs to class Simulation.

    def runSimulation(self):
        self.getNDt()
        self.ifPause = False
        if self.ifSimulationWorks == False:
            self.ifSimulationWorks = True
            while self.ifSimulationWorks:
                position = []
                velocity = []
                N = self.N
                for i in range(0, N):
                    position.append(self.Table.actualPosition(i, self.t))
                    velocity.append(self.Table.actualVelocity(i, self.t))
                ballSymbol = []
                velocityWektor = []
                for i in range(0, N):
                    ballSymbol.append(canvas.create_circle(position[i][0], position[i][1], 25, width=2,
                                                           fill=self.Table.balls[i].color, tags=('ball' + str(i))))
                canvas.update()
                canvas.after(40)
                for i in range(0, N):
                    canvas.delete(ballSymbol[i])

                self.t += 1
       
 
position and velocity arrays are used to keep position and velocity vector for all balls and t moment of simulation. Based on those values we can draw balls on screen on those positions and draw velocity vectors (however it is not implemented now). To draw balls we can use modified create_oval function:

def _create_circle(self, x, y, r, **kwargs):
    return self.create_oval(x - r, y - r, x + r, y + r, **kwargs)

tk.Canvas.create_circle = _create_circle
       
 
Modified function allows you to draw circles in the indicated position, which is the center of circle as well. Created ball symbol is stored in ballSymbol array. canvas.after(40) line causes sleep of 40 ms. It means that each t moment will last 40 ms. This value can be changed of course. If value is smaller then simulation will be more smooth, but slower because more calculations will be performing. Next, section:

                for i in range(0, N):
                    canvas.delete(ballSymbol[i])       
 
deletes all balls for t moment to make place for balls from t+1 moment.

Summary


Simulation is done according to my own idea. I hope it can be improved, code can be more concised. Maybe flag variables are not needed in this case. I am fan of simple code. I am really open to see other ways. Please leave comment if you have any suggestions.

No comments:

Post a Comment