Table Tennis

Introduction

Note

The project file for this example may be viewed/run in PFC.[1] The data files used are shown at the end of this example.

This example simulates a table tennis game.

The basic use of the wall generate command is shown. The fish callback command is used so that specific operations are performed when the ball hits the table or the net. The ball trajectory is traced during the game using the ball trace command.

Numerical Model

The “CreateTable.dat (3D)” data file creates the table and the rackets. First, the table dimensions are calculated from the domain size.

fish define dim_table
  xTable=array.create(1,2)
  yTable=array.create(1,2)
  xTable(1,1) = domain.min.x*0.95
  xTable(1,2) = domain.max.x*0.95
  Lx = math.abs(xTable(1,1))+math.abs(xTable(1,2))
  Ly = Lx*0.6
  yTable(1,1) = domain.min.y*0.95
  yTable(1,2) = yTable(1,1)+Ly
  zTable = (domain.min.z+domain.max.z)*0.5
end
[dim_table]

The table surface is then created using the wall generate command to create a planar polygonal wall (polygon keyword). The legs of the table are also created by introducing four cylindrical walls with the cylinder keyword. Walls are grouped and named (the first of the four is shown below) to facilitate later operations.

; creation of the table
wall generate group 'table' polygon           ...
            [xTable(1,2)] [yTable(1,1)] [zTable] ...
            [xTable(1,2)] [yTable(1,2)] [zTable] ...
            [xTable(1,1)] [yTable(1,1)] [zTable] ...
            [xTable(1,1)] [yTable(1,2)] [zTable]

; insertion of table legs
wall generate group 'table' cylinder        ...
     axis 0 0 -1                            ...
     base [xTable(1,2)] [yTable(1,1)] [zTable] ...
     height [Ly*0.5]                        ...
     one-wall                               ...
     radius [Lx*0.02]

Finally, the net is created as a new polygonal wall.

; creation of the net
wall generate group 'net' polygon                                   ...
     [(xTable(1,1)+xTable(1,2))*0.5] [yTable(1,1)] [zTable]           ...
     [(xTable(1,1)+xTable(1,2))*0.5] [yTable(1,2)] [zTable]           ...
     [(xTable(1,1)+xTable(1,2))*0.5] [yTable(1,1)] [zTable+Lx*0.075] ...
     [(xTable(1,1)+xTable(1,2))*0.5] [yTable(1,2)] [zTable+Lx*0.075]

Then the rackets are created and placed at the table extremities. Rackets are represented by two disk-shaped walls using the disk keyword.

; creation of the rackets
wall generate group 'player1' disk ... ;racket Player1
    dip 90                ...
    dip-direction 90      ...
    position [xTable(1,1)] [(yTable(1,1)+yTable(1,2))*0.5] [zTable+Ly*0.3] ...
    rad [Lx*0.05]

wall generate group 'player2' disk ... ;racket Player2
    dip 90                ...
    dip-direction 90      ...
    position [xTable(1,2)] [(yTable(1,1)+yTable(1,2))*0.5] [zTable+Ly*0.3] ...
    rad [Lx*0.05]
../../../../../../../_images/pingpong1.png

Figure 1: The table, the net, and rackets.

The last step in preparing the game is the creation of the ball. Its initial velocity is set on a per-component basis.

;define the initial velocity of the ball
[XballInitialVelocity = -3.0]
[ZballInitialVelocity = 2.0]

The ball trajectory during the game is traced using the ball trace command. The ball density and initial velocity are set with the ball attribute command.

;creation of the ball
ball create id 1 rad [Lx*0.005]                           ...
                 position [xTable(1,2)*0.98]              ...
                          [(yTable(1,1)+yTable(1,2))*0.5] ...
                          [zTable+Ly*0.3]
ball trace id 1
ball attribute density 10                       ...
               velocity-x [XballInitialVelocity] ...
               velocity-z [ZballInitialVelocity]

The Contact Model Assignment Table (CMAT) must be defined in each PFC model. Each contact in the model domain is assigned a contact constitutive model in this way. The default slot of the CMAT is defined for ball-facet contacts. A linear model with a Coulomb criterion associated with the contacts is chosen.

;linear model for ball-facet contacts
contact cmat default type ball-facet model linear ...
        property kn 1e6 ks 1e6 fric 0.009

To complete the preparation of the model, some function is needed to control the movement of the rackets and to stop the game in case of a fault. To do this, two FISH functions are created and registered with callback events with the fish callback command.

;set fish callbacks to check if the ball touches the net
fish callback add checknet event contact_activated
;set fish callback to prepare the racket position
fish callback add tablecontact event contact_delete

Use the program list cycle-sequence command to list the cycle points during a PFC cycle and the fish list callback-list command to list the available callback events. One can insert FISH functions between the preset cycle points during the cycle sequence.

In this example we choose to manage the ball-net and/or ball-table interactions using callback events. Regarding ball-net contacts using the contact_activated event (see “Linear Model Callback Events”), every time a contact is activated, the function checknet checks whether it is a ball-net contact. If so, the Fault! message is sent.

; check for ball-net contacts
fish define checknet(arr)
    ct=arr(1)
    ; in case of ball-facet contacts, contact.end1 refers to the ball,
    ; contact.end2 refers to the facet
    if wall.group(wall.facet.wall(contact.end2(ct))) == "net"
      io.out("Net!!")
      bp = ball.find(1)
      ball.vel(bp,1)=0
      ball.vel(bp,3)=0
    endif
end

Please be aware of the different ways one can use contact_create/delete and/or contact_activated events: a contact may be created whenever the cell extents of two pieces touch or overlap (see the 35.0 - Create/delete Contacts reference page). The contacts are initially inactive upon creation, and contact resolution is required to determine their activity state. Ball-net contacts could be created and remain inactive, and the game must continue in such a case.

Regarding ball-table contacts, every time a contact is deleted, the function tablecontact is called. If the ball is found to have touched the table, the racket of the respective player is moved to a new position that is predicted according to the theory of parabolic projectiles.

; compute the new position of the racket and move it!
fish define moverackets
  bp = ball.find(1)
  xvel = ball.vel.x(bp)
  zvel = ball.vel.z(bp)
  xpos = ball.pos.x(bp)
  zpos = ball.pos.z(bp)
  if xvel < 0
    if xpos > 0
      ball.vel(bp,1)=0
      ball.vel(bp,3)=0
      io.out("Fault!!")
    endif
    wp = wall.find(7)
    L = xTable(1,1) - xpos
  else
    if xpos < 0
      ball.vel(bp,1)=0
      ball.vel(bp,3)=0
      io.out("Fault!!")
    endif
    wp = wall.find(8)
    L = xTable(1,2) - xpos
  endif
  zM = (zvel/xvel)*L + (-9.81/2)*((L/xvel)^2)
  if zM > domain.min.z
    if zM < domain.max.z
      wall.pos.z(wp) = zM + zpos
    endif
  endif
end

;check fi=ot ball-table contacts!
fish define tablecontact(ct)
    con = ct
    ; in case of ball-facet contacts, contact.end1 refers to the ball,
    ; contact.end2 refers to the facet
    if wall.group(wall.facet.wall(contact.end2(con))) == "table"
        moverackets
    endif
end

Everything is ready … have fun!

../../../../../../../_images/pingpong2.png

Figure 2: A table tennis game.

Discussion

This tutorial presents an example of registering FISH functions with callback events to produce custom behavior (see the fish callback command). This demonstrates one of the core features of PFC: the ability to model complex phenomena using FISH.

Data Files

CreateTable.dat (3D)

; excerpt-lmiw-start
fish define dim_table
  xTable=array.create(1,2)
  yTable=array.create(1,2)
  xTable(1,1) = domain.min.x*0.95
  xTable(1,2) = domain.max.x*0.95
  Lx = math.abs(xTable(1,1))+math.abs(xTable(1,2))
  Ly = Lx*0.6
  yTable(1,1) = domain.min.y*0.95
  yTable(1,2) = yTable(1,1)+Ly
  zTable = (domain.min.z+domain.max.z)*0.5
end
[dim_table]
; excerpt-lmiw-end

; excerpt-hvqy-start
; creation of the table
wall generate group 'table' polygon           ...
            [xTable(1,2)] [yTable(1,1)] [zTable] ...
            [xTable(1,2)] [yTable(1,2)] [zTable] ...
            [xTable(1,1)] [yTable(1,1)] [zTable] ...
            [xTable(1,1)] [yTable(1,2)] [zTable]

; insertion of table legs
wall generate group 'table' cylinder        ...
     axis 0 0 -1                            ...
     base [xTable(1,2)] [yTable(1,1)] [zTable] ...
     height [Ly*0.5]                        ...
     one-wall                               ...
     radius [Lx*0.02]
; excerpt-hvqy-end

wall generate group 'table' cylinder        ...
     axis 0 0 -1                            ...
     base [xTable(1,2)] [yTable(1,2)] [zTable] ...
     height [Ly*0.5]                        ...
     one-wall                               ...
     radius [Lx*0.02]

wall generate group 'table' cylinder        ...
     axis 0 0 -1                            ...
     base [xTable(1,1)] [yTable(1,1)] [zTable] ...
     height [Ly*0.5]                        ...
     one-wall                               ...
     radius [Lx*0.02]

wall generate group 'table' cylinder        ...
     axis 0 0 -1                            ...
     base [xTable(1,1)] [yTable(1,2)] [zTable] ...
     height [Ly*0.5]                        ...
     one-wall                               ...
     radius [Lx*0.02]

; excerpt-edxd-start
; creation of the net
wall generate group 'net' polygon                                   ...
     [(xTable(1,1)+xTable(1,2))*0.5] [yTable(1,1)] [zTable]           ...
     [(xTable(1,1)+xTable(1,2))*0.5] [yTable(1,2)] [zTable]           ...
     [(xTable(1,1)+xTable(1,2))*0.5] [yTable(1,1)] [zTable+Lx*0.075] ...
     [(xTable(1,1)+xTable(1,2))*0.5] [yTable(1,2)] [zTable+Lx*0.075]
; excerpt-edxd-end

; excerpt-stlb-start
; creation of the rackets
wall generate group 'player1' disk ... ;racket Player1
    dip 90                ...
    dip-direction 90      ...
    position [xTable(1,1)] [(yTable(1,1)+yTable(1,2))*0.5] [zTable+Ly*0.3] ...
    rad [Lx*0.05]

wall generate group 'player2' disk ... ;racket Player2
    dip 90                ...
    dip-direction 90      ...
    position [xTable(1,2)] [(yTable(1,1)+yTable(1,2))*0.5] [zTable+Ly*0.3] ...
    rad [Lx*0.05]
; excerpt-stlb-end

model save 'table'

TennisTable.dat (3D)

; fname: TableTennis.dat (3D)
; The example shows how to create a table tennis game!
;
; ====================================================
model new
model large-strain on

; excerpt-wywo-start
;define the initial velocity of the ball
[XballInitialVelocity = -3.0]
[ZballInitialVelocity = 2.0]
; excerpt-wywo-end

; definition of extent and condition
model domain extent -1 1
model domain condition destroy

;creation of the table
program call 'CreateTable'

; excerpt-wywp-start
;creation of the ball
ball create id 1 rad [Lx*0.005]                           ...
                 position [xTable(1,2)*0.98]              ...
                          [(yTable(1,1)+yTable(1,2))*0.5] ...
                          [zTable+Ly*0.3]
ball trace id 1
ball attribute density 10                       ...
               velocity-x [XballInitialVelocity] ...
               velocity-z [ZballInitialVelocity]
; excerpt-wywp-end

; excerpt-bogg-start
;linear model for ball-facet contacts
contact cmat default type ball-facet model linear ...
        property kn 1e6 ks 1e6 fric 0.009
; excerpt-bogg-end


model gravity 0 0 -9.81

; excerpt-rpge-start
; check for ball-net contacts
fish define checknet(arr)
    ct=arr(1)
    ; in case of ball-facet contacts, contact.end1 refers to the ball,
    ; contact.end2 refers to the facet
    if wall.group(wall.facet.wall(contact.end2(ct))) == "net"
      io.out("Net!!")
      bp = ball.find(1)
      ball.vel(bp,1)=0
      ball.vel(bp,3)=0
    endif
end
; excerpt-rpge-end

; excerpt-ptva-start
; compute the new position of the racket and move it!
fish define moverackets
  bp = ball.find(1)
  xvel = ball.vel.x(bp)
  zvel = ball.vel.z(bp)
  xpos = ball.pos.x(bp)
  zpos = ball.pos.z(bp)
  if xvel < 0
    if xpos > 0
      ball.vel(bp,1)=0
      ball.vel(bp,3)=0
      io.out("Fault!!")
    endif
    wp = wall.find(7)
    L = xTable(1,1) - xpos
  else
    if xpos < 0
      ball.vel(bp,1)=0
      ball.vel(bp,3)=0
      io.out("Fault!!")
    endif
    wp = wall.find(8)
    L = xTable(1,2) - xpos
  endif
  zM = (zvel/xvel)*L + (-9.81/2)*((L/xvel)^2)
  if zM > domain.min.z
    if zM < domain.max.z
      wall.pos.z(wp) = zM + zpos
    endif
  endif
end

;check fi=ot ball-table contacts!
fish define tablecontact(ct)
    con = ct
    ; in case of ball-facet contacts, contact.end1 refers to the ball,
    ; contact.end2 refers to the facet
    if wall.group(wall.facet.wall(contact.end2(con))) == "table"
        moverackets
    endif
end
; excerpt-ptva-end

; excerpt-rymc-start
;set fish callbacks to check if the ball touches the net
fish callback add checknet event contact_activated
;set fish callback to prepare the racket position
fish callback add tablecontact event contact_delete
; excerpt-rymc-end

model solve time 1.0
model save 'game'
;==============================================================
;eof: TableTennis.dat (3D)

Endnotes