Bouncing smiley balls

Return to Code Snippets

Bouncing smiley balls

Postby FenrirWolf » Wed Jul 15, 2009 5:23 am

Title pretty much says all. But in detail: This example demonstrates using Panda physics and collisions to make some smiley balls bounce upon a ground plane.

I've been learning about Panda physics, so I wrote this to help me test a few things.

I also implemented cheesy shadow blob ground shadows, just so I could judge how far the balls were bouncing. :)

Code: Select all
# by FenrirWolf (David Grace) 7/2009
# Free for use by all under the Panda license

# Purpose: Demonstrates how to use Panda physics with a collisions to generate spheres which bounce on a ground
# plane.

from pandac.PandaModules import loadPrcFileData

loadPrcFileData('', '''
show-frame-rate-meter #t
//want-tk #t
//want-directtools #t''')

from pandac.PandaModules import *  # lazy git
import direct.directbase.DirectStart

from random import random

# Set up the collision traverser.  If we bind it to base.cTrav, then Panda will handle
# management of this traverser (for example, by calling traverse() automatically for us once per frame)
base.cTrav = CollisionTraverser()

# Turn on particles.  (Required to use Panda physics)

# Turn on the traverser debugger if you want to see the collisions
#base.cTrav.showCollisions (base.render)

# Having trouble figuring out what's going on with messages?  Turn this on.

# Now let's set up some collision bits for our masks
groundBit = 1
smileyBit = 2

# First, we build a card to represent the ground
cm = CardMaker('ground-card')
cm.setFrame(-60, 60, -60, 60)
card = render.attachNewNode(cm.generate())
card.lookAt (0, 0, -1)  # align upright
tex = loader.loadTexture('maps/envir-ground.jpg')

# Then we build a collisionNode which has a plane solid which will be the ground's collision
# representation
groundColNode = card.attachNewNode (CollisionNode('ground-cnode'))
groundColPlane = CollisionPlane (Plane (Vec3(0, -1, 0), Point3(0, 0, 0)))
groundColNode.node().addSolid (groundColPlane)

# Now, set the ground to the ground mask
groundColNode.setCollideMask (BitMask32().bit (groundBit))

# Why aren't we adding a collider?  There is no need to tell the collision traverser about this
# collisionNode, as it will automatically be an Into object during traversal.

# We're going to keep a list of our smileyActorNodes
smileyActors = []

# How many smileys?
maxSmileys = 25

# Our smiley base mass, roughly in kg
baseMass = 100.0

# Let's have some fun and have a PLOP! sound ready for our collisions!
plopSfx = loader.loadSfx ('audio/sfx/GUI_click.wav')

# Create a shadow card for us to use with the smileys
cm = CardMaker ('shadow-card')
cm.setFrame (-1, 1, -1, 1)
shadowCard = render.attachNewNode (cm.generate())
shadowCard.lookAt (0, 0, -1) # align upright
tex = loader.loadTexture ('maps/soft_iris.rgb')
ts = TextureStage ('blended-shadow')

# Using a little trick here...  Since the only thing resembling a shadow blob kind of texture is the
# soft_iris image that comes with Panda.  Problem is, it's opposite of what we want (ie: ring vs blob)
# So we just invert the alpha values stored in this texture and now it's a blob
ts.setCombineAlpha (TextureStage.CMModulate, TextureStage.CSPrevious, TextureStage.COSrcAlpha,
                              TextureStage.CSTexture, TextureStage.COOneMinusSrcAlpha)
shadowCard.setTexture (ts, tex)

def removeForce (smileyActor, task):
  '''Removes a temporary nudge force applied to the smiley'''
  smileyActor.node().getPhysical(0).removeLinearForce (nudgeForce) 
  return (task.done)

# -- end def removeForce

def groundCollisionEventCallback(entry):
   '''This is our ground collision message handler.  It is called whenever a collision message is triggered'''
   # Get our parent actornode
   smileyActor = entry.getFromNodePath().getParent()
   # Why do we call getParent?  Because we are passed the CollisionNode during the event and the
   # ActorNode is one level up from there.  Our node graph looks like so:
   # - ActorNode
   #   + ModelNode
   #   + CollisionNode
   # Apply the nudge force to bounce us upwards
   smileyActor.node().getPhysical(0).addLinearForce (nudgeForce)
   # set a task that will clear this force a short moment later
   base.taskMgr.doMethodLater (0.1, removeForce, 'removeForceTask', extraArgs=[smileyActor], appendTask=True)
   # PLOP!

# -- end def groundCollisionHandler

def updateShadow (gShadow, i, task):
   '''Updates the shadow cards that are under each smiley'''
   gShadow.setPos (smileyActors[i].getX(), smileyActors[i].getY(), 0.1)
   # Shadows range from 0-30 units on Z axis.  Above that, they get clamped to certain minimum size
   # Unrealistic, but means you never lose your visual cue
   dist = 1.0 - smileyActors[i].getZ() / 30.0
   z = max (0.25, dist)
   z = min (z, 0.9)
   gShadow.setScale (1.0 * z)
   gShadow.setColor (0, 0, 0, z)
   return (task.cont)
# -- end def updateShadow

# Tell the messenger system we're listening for smiley-into-ground messages and invoke our callback
base.accept ('smiley-cnode-into-ground-cnode', groundCollisionEventCallback)

for i in range (0, maxSmileys):
   # Create our smiley's physics node
   smileyActor = render.attachNewNode (ActorNode("SmileyActorNode"))
   # Load the good ole smiley face model
   smiley = loader.loadModel('smiley')
   smiley.reparentTo (smileyActor)
   # Position the smiley faces randomly in the air
   smileyActor.setPos(random() * 30 - 15, random() * 30 - 15, 100)
   # Associate the default PhysicsManager for this ActorNode
   base.physicsMgr.attachPhysicalNode (smileyActor.node())
   # Let's set some default body parameters such as mass, and randomize our mass a bit
   smileyActor.node().getPhysicsObject().setMass(baseMass + (baseMass * 0.5) * random())
   # Build a collisionNode for this smiley which is a sphere of the same diameter as the model
   smileyColNode = smileyActor.attachNewNode (CollisionNode ('smiley-cnode'))
   smileyColSphere = CollisionSphere (0, 0, 0, 1)
   smileyColNode.node().addSolid (smileyColSphere)
   # Watch for collisions with our brothers, so we'll push out of each other
   smileyColNode.node().setIntoCollideMask (BitMask32().bit (smileyBit))
   # we're only interested in colliding with the ground and other smileys
   cMask = BitMask32()
   cMask.setBit (groundBit)
   cMask.setBit (smileyBit)
   smileyColNode.node().setFromCollideMask (cMask)
   # Now, to keep the spheres out of the ground plane and each other, let's attach a physics handler to them
   smileyHandler = PhysicsCollisionHandler()
   # Set the physics handler to manipulate the smiley actor's transform.
   smileyHandler.addCollider (smileyColNode, smileyActor)
   # This call adds the physics handler to the traverser list
   # (not related to last call to addCollider!)
   base.cTrav.addCollider (smileyColNode, smileyHandler)
   # Now, let's set the collision handler so that it will also do a CollisionHandlerEvent callback
   # But...wait?  Aren't we using a PhysicsCollisionHandler?
   # The reason why we can get away with this is that all CollisionHandlerXs are inherited from CollisionHandlerEvent,
   # so all the pattern-matching event handling works, too
   smileyHandler.addInPattern ('%fn-into-%in')
   # Now, add a shadowCard instance to this smiley
   gShadow = render.attachNewNode ('shadownode')
   gShadow.setPos (smileyActor.getX(), smileyActor.getY(), 0.1)
   shadowCard.instanceTo (gShadow)
   # Start updating the shadow
   base.taskMgr.add (updateShadow, 'updateShadowTask', extraArgs=[gShadow, i], appendTask=True)
   # Add it to our tracking list
   smileyActors.append (smileyActor)
# -- end if   

# Now, let's push the smileys downwards so they will impact the ground plane.  We do so by building
# a physics force pusher that will act as constant gravity.  This is always active.
gravity = ForceNode ('globalGravityForce')
gravityNP = render.attachNewNode (gravity)
gravityForce = LinearVectorForce (0, 0, -9.8)  # 9.8 m/s gravity
gravityForce.setMassDependent (False)  # constant acceleration (set true if you think Galileo was wrong)
gravity.addForce (gravityForce)
# add it to the built-in physics manager
base.physicsMgr.addLinearForce (gravityForce)

# Let's build a world-based temporary pushing force
nudge = ForceNode ('globalNudgeForce')
nudgeNP = render.attachNewNode (nudge)
nudgeForce = LinearVectorForce (0, 0, baseMass * 150.0)  # a sizeable push up into the air
nudgeForce.setMassDependent (True)
nudge.addForce (nudgeForce)
# now, position the camera in a sane spot
base.disableMouse() (-50, -50, 50) (0, 0, 0)

# doo eet
User avatar
Posts: 267
Joined: Mon Jun 08, 2009 3:24 pm
Location: Mississippi, US

Postby astelix » Wed Jul 15, 2009 6:05 am

nice stuff to study with FenrirWolf
as an exercise for beginners:
- try to modify the code applying an attenuation over the bouncing force (nudgeForce) till the balls stops.
My Rig:
P3D 1.7.0@WinXP & Kubuntu 10.04- Athlon 64 5200 X2 ~ Radeon 3200HD (integrated)
User avatar
Posts: 866
Joined: Mon Mar 27, 2006 4:36 pm
Location: Milano, ITA

Postby FenrirWolf » Wed Jul 15, 2009 10:22 pm

astelix wrote:- try to modify the code applying an attenuation over the bouncing force (nudgeForce) till the balls stops.

Thanks. And yeah, because right now, the balls exist in a universe where there is perfect restitution. :)

Originally the balls bounced on a timed trigger, but I figured I'd toss in a demonstration of how to assign an event handler and have it trigger the bounce upon impact.
User avatar
Posts: 267
Joined: Mon Jun 08, 2009 3:24 pm
Location: Mississippi, US

Postby ambyra » Thu Jul 16, 2009 10:45 am

I would very much like to see that!
Posts: 130
Joined: Sat Sep 20, 2008 5:47 pm

Return to Code Snippets

Who is online

Users browsing this forum: No registered users and 1 guest