Bouncing smiley balls

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. :slight_smile:

# 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)
base.enableParticles()

# 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.
#messenger.toggleVerbose()

# 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')
card.setTexture(tex)

# 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)
shadowCard.setTransparency(TransparencyAttrib.MAlpha)


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!
	plopSfx.play()

# -- 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)
	gShadow.show()
	# 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()
base.camera.setPos (-50, -50, 50)
base.camera.lookAt (0, 0, 0)

# doo eet
run()

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.

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

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.

I would very much like to see that!