RigidBodyPhysics Example: Ball Shooter

From H3D.org

Jump to: navigation, search
Enlarge

This simple application demonstrates the use of RigidBodyPhysics engine. It's a ball shooting game where player control the haptics device and try to shoot the ball to the goal. We will walk you through some important notes and tips working with RigidBodyPhysics.

Please make sure you're familiar with RigidBodyPhysics X3D syntax and logic by checking the basic RigidBodyPhysics examples before reading on.

Image:Note-tip.pngThis tutorial refers to the source code. You can download it from SVN at H3D release branch, or find it at H3D/RigidBodyPhysics/examples/ballshooter/.

Alternatively, you can download the example here.


Contents

Analysis

The game is pretty simple, ball (one by one) will be displayed on the screen, and your task is to control the leg (haptics device) to kick it to the goal. Press "spacebar" for the next ball.

So what kind of objects (or more technical term, geometry nodes) do we need?

  • Balls (a bunch of them)
  • Goal
  • Walls, floor and ceiling. Basically to make the scene look nice and keep the balls within the frame view.
  • Hanger (to hold the ball for you to kick)
  • Scoreboard

Writing the X3D code

Variable prefix convention: rb (RigidBody node), trns (Transform node), shp (Shape node), gfx (Geometry node), We start with the x3d nodes. Among those above, only balls are programmatically created (Python), the rest are defined in X3D.

      <Transform DEF='trnsBalls' />
      <Transform DEF="trnsHanger">
        <Shape DEF="shpHanger">
          <Appearance>
            <Material diffuseColor="1 0 0" />
          </Appearance>
          <Box DEF="gfxHanger" size='0.15 0.01 0.08 ' />
        </Shape>
      </Transform>
      <Transform DEF="trnsGoal">
        <Shape DEF="shpGoal">
          <Appearance>
            <Material diffuseColor="0 1 0" />
          </Appearance>
          <Box DEF="gfxGoal" size='0.3 0.2 0.05' />
        </Shape>
      </Transform>
      <Transform DEF="trnsStat" translation='0 .1 -0.725'>
        <Shape DEF="shpStat">
          <Appearance>
            <Material diffuseColor="1 0 0" />
          </Appearance>
          <text DEF='txtStat' string='' solid='true'>
            <FontStyle DEF='fntStat' size='0.06' spacing='1.0' justify='MIDDLE'/>
          </text>
        </Shape>
      </Transform>
      <Transform DEF="trnsWall0">
        <Shape DEF="shpWall0">
          <Appearance>
            <Material diffuseColor=".10196 .2 0" DEF='mtWall' transparency='0' />
          </Appearance>
          <Box DEF="gfxWall0" size='.8 .05 1' />
        </Shape>
      </Transform>

And the corresponding X3D code for RigidBody sides (again I only put 1 wall there, the rest please refer to the actual source code)

      <RigidBodyCollection enabled='true' DEF='RBC' physicsEngine='ODE'>
        <CollisionCollection DEF='CC' frictionCoefficients='2 2' bounce="0.2" slipFactors='1 1' containerField='collider'>
          <CollidableShape DEF='csHanger' containerField='collidables'>
            <Shape USE='shpHanger' containerField='shape'/>
          </CollidableShape>
          <CollidableShape DEF='csGoal' containerField='collidables'>
            <Shape USE='shpGoal' containerField='shape'/>
          </CollidableShape>
          <CollidableShape DEF='csWall0' containerField='collidables'>
            <Shape USE='shpWall0' containerField='shape'/>
          </CollidableShape>
        </CollisionCollection>
        <RigidBody DEF='rbHanger' fixed='true' mass="0.05" position="0 0 0">
          <Geometry USE='csHanger' containerField='geometry'/>
          <Box USE='gfxHanger' containerField='massDensityModel'/>
        </RigidBody>
        <RigidBody DEF='rbGoal' fixed='true' mass=".1" position="0 -0.1 -0.725">
          <Geometry USE='csGoal' containerField='geometry'/>
          <Box USE='gfxGoal' containerField='massDensityModel'/>
        </RigidBody>
        <RigidBody DEF='rbWall0' fixed='true' mass="1" position="0 -.3 -.3">
          <Geometry USE='csWall0' containerField='geometry'/>
          <Box USE='gfxWall0' containerField='massDensityModel'/>
        </RigidBody>
      </RigidBodyCollection>

Fully control the nodes created in Python

When using createX3DNodeFromString/Url, a dictionary storing all DEFed nodes will be returned, we simply put these into a global dictionary:

objs = {} # global object dictionary
def RegisterObjs(dn) :
  global objs
  for key, value in dn.items():
    objs[key] = value
 

then when creating a node:

  ballname = 'Ball' + str(counter)
  trnsBall, dn = createX3DNodeFromString("""<Transform DEF='trns"""+ballname+"""'>
                                              <Shape DEF='shp"""+ballname+"""'>
                                                <Appearance>
                                                  <FrictionalSurface />
                                                  <Material diffuseColor="1 0 0" />
                                                </Appearance>
                                                <Sphere DEF='gfx"""+ballname+"""' radius='"""+str(GameConst.BALL_RADIUS)+"""' />
                                              </Shape>
                                            </Transform>""")
  # add to global dictionary
  RegisterObjs(dn)
 

Then we can use objs['trnsWall0'] to refer to the wall0's Transform node, objs['gfxBall1'] to ball1's Geometry node, etc.

Trigger when ball hits goal

Because there's no OnHit event for the goal (in fact there is no event-system for a RigidBody node at all), we have to manually check it. One solution is: for every ball generated, a route is attached to it to track the change in position. Below is an implementation of the route.

# Class handle when the goal is hit (i.e new score made), routed from rbBall.position
class GoalHit(AutoUpdate(SFVec3f)):
  def update(self, event):
    global game, objs
    ball_pos = event.getValue()
    if self.BallHitGoal(ball_pos) and game.started: # new score
      game.score += 1
      DisplayScoreboard()
      objs['rbBall'+str(game.balls)].position.unroute(goalhit)    # remove route to avoid double collision
    return Vec3f(0,0,0)
 
  def BallHitGoal(self, ball_pos):
    " collision checking, see if the ball has hit the goal "
    global rbGoal
    goal_pos = rbGoal.position.getValue()
    goal_dx = rbGoal.massDensityModel.getValue().size.getValue().x / 2
    goal_dy = rbGoal.massDensityModel.getValue().size.getValue().y / 2
    goal_dz = rbGoal.massDensityModel.getValue().size.getValue().z / 2
    if (goal_pos.x - goal_dx <= ball_pos.x <= goal_pos.x + goal_dx) and \
        (goal_pos.y - goal_dy <= ball_pos.y <= goal_pos.y + goal_dy):
      d = ball_pos.z - goal_pos.z
      if (d >= 0 and d <= GameConst.BALL_RADIUS + goal_dz + 0.001):
        return True
    return False
goalhit = GoalHit()

so when we create a new ball, add a route to it

  rbBall.position.route(goalhit)

Performance boosts: removing old balls

Due to the nature of the above routes (route from rbBall.position), the program checks for every single movement, this eats up the CPU resources unnecessarily as the number of balls increase. One solution for that is to remove the old balls (floating on the floor). The below code does just that: remove ball n-x when it's ball n's turn.

# inside def NewBall()
  if id > 0:
    ballname = 'Ball'+str(id)
    RemoveMFItem(trnsBalls.children, objs["trns"+ballname])
    RemoveMFItem(rbc.bodies, objs["rb"+ballname])
    RemoveMFItem(cc.collidables, objs["cs"+ballname])
    objs["rb"+ballname].position.unroute(goalhit)
  #endif

And function RemoveMFItem:

def RemoveMFItem(mf, val):
  list = mf.getValue()
  list.remove(val)
  mf.setValue(list)
Personal tools
go to