RigidBodyPhysics Example: Ball Shooter
From H3D.org
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.
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)

