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 | |
---|---|
Variables | Description |
t | time |
dt | time step |
x0,y0 | initial coordinates of the ball (t=0) |
vx0,vy0 | initial velocity of the ball (t=0) |
x,y | current ball coordinates |
vx,vy | current ball velocity |
R | radius of the ball |
id | ball number (id) |
color | ball color, for example "blue" |
collisionBallNr | id of ball that collids with current ball |
ifCollisionCompleted | variable of Boolean type, returns True if collision with ball is completed |
Methods | Description |
nextStep | calculates next ball position after dt (according to equation (0) from part 1) |
ifCollisionWithBall | check if collision with ball happened, if yes - it calculates new velocities for it |
ifCollisionWithEdge | check if collision with edge of the table happened, if yes - it calculates new velocities for it |
setInitialBallVelocity | set random initial velocity for the ball |
Class: Table | |
---|---|
Variables | Description |
L, W | table dimentions |
borderCoordinatesX | array contains 2 edge values of x variable - that are: 0 and L |
borderCoordinatesY | array contains 2 edge values of y variable - that are: 0, W |
N | number of balls |
balls | array that contains Ball objects |
colors | array contains name of colors |
Methods | Description |
setInitialBallsPositions | set initial random positions for all balls, in a way that they do not overlap themself |
actualPosition | run next step of simulation, i.e. run methods: nextStep, ifCollisionWithEdge, ifCollisionWithBall, returns actual position after the step |
actualVelocity | auxiliary method, returns actual velovity of selected ball |
getInitialPositionsAndVelocities | auxiliary method, returns initial velocities of all balls |
Class: Simulation | |
---|---|
Variables | Description |
t | time |
ifSimulationWorks | auxiliary variable of boolean type |
ifPause | auxiliary variable of boolean type |
Methods | Description |
getNDt | gets N and dt variables from input fields |
runSimulation | runs simulation |
stop | stops 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:
- Set initial values for each ball (position and velocity)
- Calculate new position according to equation (0) from part 1
- Check if collision with another ball or with the edge of the table occures (if yes - calculate new velocity)
- Go back to step 2.
- Ad 1. Velocity initialisation (in class Ball):
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 setInitialBallVelocity(self): self.vx0 = random.randint(-10, 10) self.vy0 = random.randint(-10, 10) self.vx = self.vx0 self.vy = self.vy0
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).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
- 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.
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):
- Calculate distance between ball and another ball (for loop over all balls)
- 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
- Calculate $\alpha$ angle. It is related to arctan function as shown in part 1.
- Calculate normal and tangent component of velocity vector for both collided balls (use equation (1b) from part 1.)
- Swap normal component of velocity vector between balls.
- 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.