Actor instancing sample 2

After the previous sample I decided to make another one that shows how to switch between unique and cloned actors, assigned to each character onscreen.
In this sample each character changes its animation (and actor) every 2 seconds. Running Ralphs use clones (they are red), walking Ralphs use own unique actors (they are green).

from pandac.PandaModules import *
from direct.actor.Actor import Actor
from direct.gui.DirectGui import OnscreenText
import random, sys

screenWidth, screenHeight = 500, 500

# Apply settings and run Panda
loadPrcFileData("", "win-size %d %d" % (screenWidth, screenHeight))
loadPrcFileData("", "sync-video #f")
import direct.directbase.DirectStart

base.setBackgroundColor(0, 0, 0.2)
base.setFrameRateMeter(True)
base.disableMouse()

# Helper function
def addInstructions(pos, msg):
    return OnscreenText(text=msg, style=1, fg=(1,1,1,1), mayChange=1,
                        pos=(-1, pos), align=TextNode.ALeft, scale = .05,
                        shadow=(0,0,0,1), shadowOffset=(0.1,0.1))

# Readout and instructions
inst1 = addInstructions(0.95, 'Down/Up Arrow : descrease/increase the number of characters (currently: unknown)')
inst2 = addInstructions(0.90, 'Escape : quit')
inst3 = addInstructions(0.85, 'Currently, there are N actors in the pool, that are available for instancing')

# Setup camera
base.camera.setPos(0, 0, 10)
base.camera.lookAt(0, 0, 0)
lens = OrthographicLens()
lens.setFilmSize(screenWidth/10, screenHeight/10)
base.cam.node().setLens(lens)

# The actual script
characters = []

class Pool():
    def __init__(self):
        self.pool = []
        self.lastGoodFrame = 3
    def getActor(self):
        # If we found an actor in the pool with the current frame
        # between 0 and self.lastGoodFrame, just use it:
        for actor in self.pool:
            if actor.getCurrentFrame("run") <= self.lastGoodFrame:
                return actor
        # If such actor wasn't found, create new one:
        return self._newActor()
    def _newActor(self):
        actor = Actor("samples/Roaming-Ralph/models/ralph",
                    {"run":"samples/Roaming-Ralph/models/ralph-run"})
        actor.flattenStrong()
        actor.postFlatten()
        actor.loop("run")
        self.pool += [actor]
        return actor
pool = Pool()

class Character():
    def __init__(self):
        self.placeholder = render.attachNewNode("placeholder")
        x = (random.random()-0.5) * screenWidth/10
        y = (random.random()-0.5) * screenHeight/10
        h = random.random() * 360
        self.placeholder.setPosHpr(x, y, 0, h, 0, 0)

        # Create own unique actor (but we won't use it right away).
        self.ownActor = self._makeOwnActor()
        # Get clone from pool and display it.
        clone = self._getActorFromPool()
        self.visibleModel = clone.instanceTo(self.placeholder)
        self.placeholder.setColor(1, 0, 0)

        self.usingClone = True # whether we use clone or unique actor now
        self.lastAttempt = 0 # time when we attempted to change animation
        self.updateTask = taskMgr.add(self.animate, "animateTask")

    def _makeOwnActor(self):
        actor = Actor("samples/Roaming-Ralph/models/ralph",
                    {"walk":"samples/Roaming-Ralph/models/ralph-walk"})
        actor.loop("walk")
        return actor
    def _getActorFromPool(self):
        global pool
        actor = pool.getActor()
        return actor

    def animate(self, task):
        elapsed = task.time - self.lastAttempt
        if elapsed >= 2:
            # We change unique actor to clone and back every 2 seconds.
            self._changeAnimation()
            self.lastAttempt = task.time
        return task.cont
    def _changeAnimation(self):
        if self.usingClone:
            self._changeCloneToActor()
            self.usingClone = False
        else:
            self._changeActorToClone()
            self.usingClone = True
    def _changeCloneToActor(self):
        self.visibleModel.detachNode()
        self.visibleModel = self.ownActor
        self.visibleModel.reparentTo(self.placeholder)
        self.placeholder.setColor(0, 1, 0) # Unique actors are green
    def _changeActorToClone(self):
        self.visibleModel.detachNode()
        clone = self._getActorFromPool()
        self.visibleModel = clone.instanceTo(self.placeholder)
        self.placeholder.setColor(1, 0, 0) # Clones are red

    def destroy(self):
        if self.usingClone:
            self.visibleModel.detachNode()
        else:
            # Actor has to be cleaned before removing:
            self.visibleModel.cleanup()
            self.visibleModel.detachNode()
        taskMgr.remove(self.updateTask)

def addCharacter():
    global characters
    character = Character()
    characters += [character]
    updateReadout()
def removeCharacter():
    global characters
    if len(characters) == 1:
        return
    character = characters.pop()
    character.destroy()
    updateReadout()

def updateReadout():
    global characters, pool, inst1, inst3
    inst1.setText('Down/Up Arrow : descrease/increase the number of characters (currently: %d)' % len(characters))
    inst3.setText('Currently, there are %d actors in the pool, that are available for instancing' % len(pool.pool))

addCharacter()

# Controls
base.accept("escape", sys.exit)

base.accept("arrow_up", addCharacter)
base.accept("arrow_down", removeCharacter)

run()

Confirmed working on Mac OS X.5 with 1.6.0pre. ~60 FPS @ 70 characters.