Ground Collision is to slow

I have worked a lot the last days to get ground collisions working. But it appears to be to too slow…

I copied the Roaming-Ralph-method, but with more ralphs. I’ve tried CollisionHandlerFloor before with the same results.

With the Roaming-Ralph-Map, which has an own collide mesh*, I have a framerate between 10 an 18 with 8 Ralphs. With a huge map with very simple geometry, but no collision mesh, the framerate is between 0 an 1. In Roaming Ralph, my framerate is between 30 an 45, depending on the current collisions.

  • (but i can’t find any tag inside the egg file, as mentioned in the sample, so maybe it checks against visible geometry)

I am using listings for storing the eight Ralphs, and I have a lot of ‘for player in list’ statements, which aren’t very fast, but on the huge map, the framerate is nearly zero, so that isn’t the point here.

You can test the following code inside the Roaming-Ralph-Sample-Folder. The Skydome is from here: http://www.mygamefast.com/volume1/issue3/5/, if you want to use it.

I would be very glad, if anyone can show me a smart, fast way for doing ground collisions with uneven terrain, which maybe contains overhanging cliffs and caves, for a lot of Actors (maybe 50 or 100).

Here is the whole code, but at the moment only the parts with ‘setupCollision’ and ‘setPlayersZ’ are important.

# Catch Me!
# -*- encoding: utf-8 -*-
from direct.showbase.ShowBase import ShowBase
from direct.showbase.PythonUtil import pdir  # for debugging
from direct.gui.OnscreenText import OnscreenText
from direct.actor.Actor import Actor
from direct.task import Task
from panda3d.core import Point3, Vec3, Vec4, BitMask32
from panda3d.core import PandaNode, NodePath, Camera, TextNode
from panda3d.core import AmbientLight, DirectionalLight, Fog
from panda3d.core import CollisionTraverser, CollisionNode
from panda3d.core import CollisionHandlerQueue, CollisionRay
from panda3d.ai import AIWorld, AICharacter, AIBehaviors
from pandac.PandaModules import CompassEffect
from random import randrange, choice

## Global Variables ##

# color for the catchers
CATCHCOLOR = Vec4(1, .2, .2, 1)
# used by catchers: Ignore targets, that aren't nearer than this, compared to
# the current target
IGNORE_NEAR = 5
# time in sec. between each check for nearest target
CATCHER_CHECK_TIME = 3.0
# length of the Field of View
CAM_MAXVIEW = 1000
# angle of the Field of View, default is 40
CAM_DEGREE = 60
BMASK_OFF = BitMask32.allOff()
BMASK_GROUND = BitMask32.bit(0)

## helper functions ##

# function to put instructions on the screen.
def addInstructions(line, message):
    pos = 1 - (line * .05)
    return OnscreenText(text=message, style=1, fg=(1,1,1,1),
                        pos=(-1.3, pos), align=TextNode.ALeft, scale=.05)

# function to put title on the screen.
def addTitle(text):
    return OnscreenText(text=text, style=1, fg=(1,1,1,1),
                        pos=(1.3,-0.95), align=TextNode.ARight, scale=.07)

## main World class ##

class World(ShowBase):

    def __init__ (self):
        """
        Setup the World.

        """
        ShowBase.__init__(self)
        self.keyMap = {}  # will be filled later
        base.setFrameRateMeter(True)  # show framerate
        addTitle('Catch Me!')
        addInstructions(1, "Just Watch!")
        self.actors = {'players':8, 'catchers':1, 'catched':0}
        self.world = render.attachNewNode('The World')  # the main Node

        # calling all setups
        self.setupAmbiente()
        self.setupCamera()
        self.setupMeshes()
        self.setupCollisions()
        #self.setupKeyMap()
        self.setupAi()
        self.setupDebug(True)
        self.setupUpdates()


    def setupAmbiente(self):
        """
        Game atmospheric, nothing important here.

        """
        # Fog to hide the distance limit
        color = (0.73,0.71,0.85)
        distance_fog = Fog("distance fog")
        distance_fog.setColor(*color)
        distance_fog.setLinearRange(200, CAM_MAXVIEW - (CAM_MAXVIEW // 16))
        self.world.setFog(distance_fog)
        base.setBackgroundColor(*color)
        '''
        # dome shaped skydome
        sky_np = NodePath('Skydome')
        sky_np.node().setIntoCollideMask(BMASK_OFF)
        skydome = loader.loadModel('models/sky')
        skydome.setEffect(CompassEffect.make(self.world))
        skydome.setScale(CAM_MAXVIEW + CAM_MAXVIEW // 20)
        skydome.setZ(- CAM_MAXVIEW // 16)
        skydome.reparentTo(sky_np)
        sky_np.reparentTo(self.camera)  # NOT to render
        '''
        # lighting
        ambientLight = AmbientLight("ambientLight")
        ambientLight.setColor(Vec4(.15, .15, .15, 1))
        directionalLight = DirectionalLight("directionalLight")
        directionalLight.setDirection(Vec3(0,-10,-10))
        directionalLight.setColor(Vec4(.85, .85, .8, 1))
        directionalLight.setSpecularColor(Vec4(.2, .2, .2, 1))
        self.world.setLight(self.world.attachNewNode(ambientLight))
        self.world.setLight(self.world.attachNewNode(directionalLight))

    def setupCamera(self):
        """
        Adjust the main camera. Currently standart mouse movement.

        """
        base.camera.reparentTo(self.world)
        self.camLens.setFar(CAM_MAXVIEW)
        self.camLens.setFov(CAM_DEGREE)

    def setupMeshes(self):
        """
        Load and position environment & players (based on self.actors).

        Create:
        self.catcher_np_list: NodePaths of all catchers, used for moving & AI
        self.hunted_np_list: same for the targets
        self.catcher_act_list: only for animations!
                                                 Else scaling would cause a Bug
        self.hunted_act_list: same for the targets

        self.players_list: NodePaths of all players
        self.players_pos_list: last valid position of each player

        """
        # environment
        self.envir = loader.loadModel("models/world")
        #self.envir.setSz(.6)
        self.envir.reparentTo(self.world)
        #self.envir.find('**/start_point').getPos()
        #self.envir.place()

        # standart player
        self.player_args = ("models/ralph",
                        {"run":"models/ralph-run", "walk":"models/ralph-walk"})

        # load & pos players
        self.catcher_np_list, self.catcher_act_list = \
                       self._initActors(self.actors["catchers"], 3, CATCHCOLOR)
        self.hunted_np_list, self.hunted_act_list = \
             self._initActors(self.actors["players"] - self.actors["catchers"])
        self.players_list = self.hunted_np_list[:] + self.catcher_np_list
        self.players_pos_list = \
                              [player.getPos() for player in self.players_list]

    def _initActors(self, number, area=10,
                   color=None, scale=0.12, actor_args=None):
        """
        Return a tuple of two listings: Actor NodePath and the Actors.
        There are randomly positioned in radiant of area and are childs
        of self.world. actor_args is a tuple of the arguments that Actor()
        needs for creation.

        """
        if actor_args is None: actor_args = self.player_args
        area = round(area)
        np_list = []
        actor_list = []
        for i in range(number):
            np_list.append(NodePath('Player NodePath'))
            actor_list.append(Actor(*actor_args))
            actor_list[i].setScale(scale)
            actor_list[i].setCollideMask(BMASK_OFF)
            position = Vec3(i*2 + randrange(-area, area+1),
                       i*2 + randrange(-area, area+1), 10)
            np_list[i].setPos(position)
            np_list[i].reparentTo(self.world)
            actor_list[i].reparentTo(np_list[i])
            if color is not None: actor_list[i].setColor(color)
        return (np_list, actor_list)

    def setupCollisions(self):
        """
        Using self.players_list.

        Create:
        self.ground_traverser: currently the main Traverser
        self.col_np_players_list: ground collision NodePaths for all players
        self.col_ground_queues: list of Handlers for ground collisions

        """
        # create Traverser
        self.ground_traverser = CollisionTraverser()

        # create CollisionNodes and Handlers
        self.col_np_players_list, self.col_ground_queues = \
               self._initGroundCols(self.players_list, BMASK_GROUND, BMASK_OFF)
        # add the Handlers to the Traverser
        for col_np, handler in zip(self.col_np_players_list,
                                                       self.col_ground_queues):
            self.ground_traverser.addCollider(col_np, handler)

    def _initGroundCols(self, nodePath_list, from_mask, into_mask):
        """
        Return a tuple of two listings: a list of all CollisionNodePaths
        and a list of the CollsionHandlerQueues.

        """
        np_list, handler_list = [], []
        for player in nodePath_list:
            col_node = CollisionNode('ColNode')
            col_node.addSolid(CollisionRay(0, 0, 2, 0, 0, -1))
            col_node.setFromCollideMask(from_mask)
            col_node.setIntoCollideMask(into_mask)
            np_list.append(player.attachNewNode(col_node))
            handler_queue = CollisionHandlerQueue()
            handler_list.append(handler_queue)

        return (np_list, handler_list)

    def setupAi(self):
        """
        Currently very basic and not compatible with the ground collisions. But
        it moves the players for a short time, until there are all stuck.

        Create:
        self.ai_world_catch: This contains all AI-Characters

        """
        # main AI World
        self.ai_world_catch = AIWorld(self.world)
        # AI Characters of hunted / targets and their empty Behaviors
        self.ai_chars_hunted, self.ai_hunted_behaviors = \
                     self._initAiActors(self.hunted_np_list, 'hunted', speed=8)
        # AI Characters of catchers and their empty Behaviors
        self.ai_chars_catchers, self.ai_catchers_behaviors = \
                  self._initAiActors(self.catcher_np_list, 'catcher', speed=18)
        # targets evade catchers
        for i in range(len(self.hunted_np_list)):
            for j in range(len(self.catcher_np_list)):
                self.ai_hunted_behaviors[i].evade(self.catcher_np_list[j],
                                                  15, 30, .8)
            self.ai_hunted_behaviors[i].wander(5, 0, 50, .2)
            self.hunted_act_list[i].loop('run')
        # catchers pursue their nearest target
        for i in range(len(self.catcher_act_list)):
            target = choice(self.hunted_np_list)
            self.ai_catchers_behaviors[i].pursue(target, .8)
            self.ai_catchers_behaviors[i].wander(2, 0, 50, .2)
            self.catcher_act_list[i].loop('run')

    def _initAiActors(self, char_list, name, mass=60, movt=.05, speed=12):

        """
        Return a tuple of two listings. First the AICharacters, second the
        AIBehaviors.

        """
        ai_list, behav_list = [], []
        number = len (char_list)
        for i in range(number):
            ai_list.append(AICharacter(name,
                    char_list[i], mass, movt, speed))
            self.ai_world_catch.addAiChar(ai_list[i])
            behav_list.append(ai_list[i].getAiBehaviors())
            #behav_list[i].initPathFind("models/navmesh.csv")  # no navmesh yet
        return (ai_list, behav_list)

    def setupDebug(self, debug_mode=False):

        if not debug_mode: return

        # show all collisions
        self.ground_traverser.showCollisions(render)
        for col_np in self.col_np_players_list:
            col_np.show()

    ## Task section ##

    def setupUpdates(self):
        """
        Setup all Tasks that are known at startup.

        """
        taskMgr.add(self.playersUpdate, 'Players Moves')
        taskMgr.doMethodLater(CATCHER_CHECK_TIME, self.checkCatcherTarget,
                              'check nearest Target')
        taskMgr.add(self.setPlayersZ, 'earthing')

    def playersUpdate(self, task):
        """
        Saves the current player positions as valid, then moves all
        AI-Characters.

        """
        self.players_pos_list = \
                              [player.getPos() for player in self.players_list]
        self.ai_world_catch.update()
        return Task.cont

    def checkCatcherTarget(self, task):
        """
        Check for all catchers: is the current target the nearest target?
        Consider IGNORE_NEAR.

        Currently a placeholder with random effect.

        """
        counter = 0
        for catcher in self.catcher_np_list:
            current_distance = randrange(1,21)  # placeholder
            target, new_distance = self.getNearestTarget(catcher)
            # need to change the target?
            if current_distance + IGNORE_NEAR < new_distance:
                self.ai_catchers_behaviors[counter].removeAi('pursue')
                self.ai_catchers_behaviors[counter].pursue(target, .8)
            counter += 1
        return Task.again

    def getNearestTarget(self, catcher):
        """
        Return a tuple with the nearest target and its distance.

        Currently a placeholder with random effect.

        """
        target = choice(self.hunted_act_list)  # placeholder
        new_distance = randrange(6,26)  # placeholder
        return target, new_distance

    def setPlayersZ(self, task):
        """
        Update the Z-coordinate for all players. Very slow, need to improved.

        """
        self.ground_traverser.traverse(self.world)
        for ground_handler, player, player_pos in \
         zip(self.col_ground_queues, self.players_list, self.players_pos_list):
            entries = []
            for i in range(ground_handler.getNumEntries()):
                entries.append(ground_handler.getEntry(i))
            entries.sort(lambda x,y: cmp(y.getSurfacePoint(render).getZ(),
                                         x.getSurfacePoint(render).getZ()))

            if (len(entries)>0) and (entries[0].getIntoNode().getName() == \
                                                                    "terrain"):
                player.setZ(entries[0].getSurfacePoint(render).getZ())
            else:
                player.setPos(player_pos)
        return Task.cont


    def setupKeyMap(self):
        pass

world = World()
world.run()

Try searching the forums for quadtree or octree.

Thank you, that’s the solution I want :smiley:

I tried the ocquadtreefy-script from here:
https://github.com/nomad321/eggOctree
and add the needed imports, but don’t know how to merge the changes back to the original branch from treeform, maybe I don’t have the permissions to do it.

The collision mesh I get with this doesn’t fit to the original mesh, it ist vertically and seems to use a different coordinate system. I am still figuring out the right position, which isn’t that easy, because col_nodepath.show() doesn’t show me the collision mesh :frowning:

I have a second question about ground collision: How to create a CollisionFloorMesh.
It’s optimised for ground collisions and test only for the Z axis. I tried the code below, but it crashes python. Maybe because the vertices of the triangles are not given in the correct order, maybe they are to much. What I’ve tried here: to split a mesh in triangles and get the vertices for each one, then create a CollisionFloorMesh with it.

def makeColFloorMesh(self, model):  # Crashes Python
        """
        Make a CollisionFloorMesh-Object out of a model. The model must only
        contains triangles as geometrical primitives.

        """
        # get GeomNode
        col_floor_mesh = CollisionFloorMesh()
        geom_node = model.find('**/+GeomNode').node()
        geom_node.decompose()  # split in triangles
        geoms = geom_node.getGeoms()
        print (geoms)

        # get single Geoms
        for geom in geoms:
            primitives = geom.getPrimitives()
            vdata = geom.getVertexData()
            vertex = GeomVertexReader(vdata, 'vertex')
        # get single Primitives
        for prim in primitives:
            for p in range(prim.getNumPrimitives()):
                start, end = prim.getPrimitiveStart(p), prim.getPrimitiveEnd(p)
                args = []
                counter = 0
                # get single triangle
                for i in range(start, end):
                    vi = prim.getVertex(i)
                    vertex.setRow(vi)
                    v = vertex.getData3f()
                    x, y, z = v.getX(), v.getY(), v.getZ()
                    # create CollisionFloorMesh
                    if counter == 0:
                        col_floor_mesh.addVertex(Point3(v))
                    elif counter <= 2:
                        col_floor_mesh.addTriangle(x, y, z)
                    elif counter > 2:
                        raise ValueError ('mkfloor: This is no Triangle!')
                    counter += 1

        return col_floor_mesh