Physics + Collisions issue

Hello,
I am trying to create a scene in Panda3D with a top-down camera and click-to-move functionality. It is working almost as intended, however there seem to be some issues with the character. Initially, it falls to the floor, however when I click anywhere on the map, it moves to the right direction but it also falls through the floor down to a certain point, which seems to be dependent on the initial position of the character. For example, when the initial Z of the character is set to 21.8, it falls to around -21.3875, when it is set to 31.8, it falls to around -31.3875, like so:

After clicking, the positions are as follows:

The PLAYER POS represents the position of the Actor (player.character), while the PHYS PLAYER POS represents the position of the physics node (player.actorNP)

Here is a video showing the issue:
https://youtu.be/H4nxoWlKGbI

Here is my source code:
main.py:

"""

Panda3D + RenderPipeline learning project

"""
# Insert the pipeline path to the system path, this is required to be
# able to import the pipeline classes
pipeline_path = "RenderPipeline"

# Import system libraries
import os
import sys
import struct
from math import pi, sin, cos
import constants as const

# Add pipeline libraries to path
sys.path.insert(0, pipeline_path)

# Import Panda3D libraries - panda3d.core and direct packages
from panda3d.core import *
from panda3d.physics import *
from direct.gui.OnscreenText import OnscreenText
from direct.actor.Actor import Actor
from direct.showbase.ShowBase import ShowBase
from direct.task import Task
from direct.interval.IntervalGlobal import Sequence
from direct.interval.ActorInterval import ActorInterval

# Import the RenderPipeline libraries
from rpcore import RenderPipeline

# Import the player classes
from characters import PlayerCharacter

'''
    A list of configuration files from which we will read the game settings:
        * config.prc                - General game and display configurations, such as resolution, fullscreen, etc
        * @TODO: keybindings.prc    - A config file with key bindings
'''
configfiles = [
    "config.prc"
]
'''
    @TEMPORARY
    The main player character dictionary, containing the location of the model and a list of all its animations

    @TODO: move it to a separate character/player class
'''
player = {
    'model'         : 'resources/Guard/Guard',
    'animations'    : {
        "run"       : "resources/Guard/run",
        "walk"      : "resources/Guard/walk",
        "dance"     : "resources/Guard/dance",
        "guitar"    : "resources/Guard/guitar",
        "idle"      : "resources/Guard/idle",
        "leg-sweep" : "resources/Guard/leg-sweep",
        "run-jump"  : "resources/Guard/run-jump"
    }
}


class Application(ShowBase):
    '''
        Initialize and setup the application
    '''
    def __init__(self):

        # Load configuration file - Window size, title, fullscreen, etc
        self.loadConfig()
        # Define the key bindings
        self.defineKeys()
        # Init the RenderPipeline
        self.initRenderPipeline()
        # Setup the environment
        self.initEnvironment()
        # Setup the physics
        self.initPhysics()
        # Setup the player actor
        self.initPlayer()
        # Setup the camera
        self.initCamera()
        # Setup the lighting
        self.initLighting()
        # Setup mouse click handler
        self.initMouseHandler()

        taskMgr.add(self.updateCamera, "updateCameraTask")


    '''
        Initialize the RenderPipeline
    '''
    def initRenderPipeline(self):
        # Construct the render pipeline
        self.render_pipeline = RenderPipeline()
        self.render_pipeline.create(self)
        self.render_pipeline.daytime_mgr.time = "07:00"

        # Use a special effect for rendering the scene, this is because the
        # roaming ralph model has no normals or valid materials
        self.render_pipeline.set_effect(render, "scene-effect.yaml", {}, sort=250)

        # Declare variables.
        base.win.setClearColor(Vec4(0,0,0,1))


    '''
        Initialize the environment
    '''
    def initEnvironment(self):
        # Load the scene
        self.environ = loader.loadModel("resources/world")
        self.environ.reparentTo(render)
        self.environ.setPos(0,0,0)

        # Remove wall nodes
        self.environ.find("**/wall").remove_node()

        # Add collision mask
        self.environ.find("**/terrain").setCollideMask(const.FLOOR_MASK)



    '''
        Initialize the player, with its model and animations.
        Afterwards, loop the idle animation.
    '''
    def initPlayer(self):
        # Create the main character
        # Set his position a bit higher, so we can see him fall
        initialPosition = Vec3(-110.9, 29.4, 21.8)
        self.player     = PlayerCharacter(player, initialPosition)

        # Add the character physics and collisions
        self.player.addCharacterPhysics()
        self.player.addCharacterCollisions()
        self.playerPos  = self.player.getPos()


    '''
        We will detect the height of the terrain by creating a collision ray and casting it downward toward the terrain. One ray will start above ralph's head, and the other will start above the camera. A ray may hit the terrain, or it may hit a rock or a tree. If it hits the terrain, we can detect the height. If it hits anything else, we rule that the move is illegal.
    '''
    def initPhysics(self):
        self.cTrav          = CollisionTraverser()
        self.physicsHandler = PhysicsCollisionHandler()
        self.queueHandler   = CollisionHandlerQueue()

        # Enable particle system. It is needed in order to enable physics
        base.enableParticles()
        # Add gravity to the whole environment
        gravityFN       = ForceNode('world-forces')
        gravityFNP      = render.attachNewNode(gravityFN)
        gravityForce    = LinearVectorForce(0,0,-9.81) #gravity acceleration
        gravityForce.setMassDependent(False)
        gravityFN.addForce(gravityForce)
        base.physicsMgr.addLinearForce(gravityForce)

    '''
        Initialize the camera
    '''
    def initCamera(self):
        # Disable the default camera controls
        base.disableMouse()
        # Set the initial camera position
        base.camera.setPos(
            self.player.getActor().getX() + 10,
            self.player.getActor().getY() + 10,
            self.player.getActor().getZ() + 15
        )
        # Look at the player
        base.camera.lookAt(self.player.getActor())
        # ???
        base.camLens.setFov(80)


    '''
        Create the initial, basic lighting
    '''
    def initLighting(self):
        # Create some lighting
        ambientLight = AmbientLight("ambientLight")
        ambientLight.setColor(Vec4(.3, .3, .3, 1))
        directionalLight = DirectionalLight("directionalLight")
        directionalLight.setDirection(Vec3(-5, -5, -5))
        directionalLight.setColor(Vec4(1, 1, 1, 1))
        directionalLight.setSpecularColor(Vec4(1, 1, 1, 1))
        render.setLight(render.attachNewNode(ambientLight))
        render.setLight(render.attachNewNode(directionalLight))


    '''
        Initialize the mouse click collision handler
    '''
    def initMouseHandler(self):
        self.clickNode   = CollisionNode('mouseRay')
        self.clickNode.setFromCollideMask(const.FLOOR_MASK)
        self.clickNode.setIntoCollideMask(const.MASK_OFF)
        self.clickNP     = camera.attachNewNode(self.clickNode)
        self.clickNode.setFromCollideMask(GeomNode.getDefaultCollideMask())
        self.clickRay    = CollisionRay()
        self.clickNode.addSolid(self.clickRay)
        self.cTrav.addCollider(self.clickNP, self.queueHandler)


    '''
        Get the mouse click position
    '''
    def getMousePosition(self, mousepos):
        self.clickRay.setFromLens(base.camNode, mousepos.getX(),mousepos.getY())
        self.cTrav.traverse(render)
        if self.queueHandler.getNumEntries() > 0:
            self.queueHandler.sortEntries()
            self.position = self.queueHandler.getEntry(0).getSurfacePoint(self.environ)
            return None



    '''
        Move the player to the clicked position
    '''
    def movePlayerToPosition(self):
        self.getMousePosition(base.mouseWatcherNode.getMouse())
        self.player.moveTo(self.position)
        return



    '''
        Define the keys to be used in this game.
        Later, we will add a dynamic config in a file, which we can change in-game
    '''
    def defineKeys(self):
        # Define an exit key
        self.accept("escape", sys.exit)
        # Define the left mouseclick as a moving key
        self.accept('mouse1', self.movePlayerToPosition)


    '''
        Accepts defined keys to move the player
        Also deals with grid checking and collision detection
        TODO: finish character movement
    '''
    def updateCamera(self, task):
        self.cTrav.traverse(render)
        self.player.debug()
        self.player.update()
        if (self.playerPos != self.player.getPos()):
            self.playerPos = self.player.getPos()

            base.camera.setPos(
                self.player.getX() + 10,
                self.player.getY() + 10,
                self.player.getZ() + 15
            )

        # Return task.cont so that the task runs continuously. Otherwise, it will just run once
        return task.cont


    """
        Loads all the needed configuration files, which are stored in the
        global configfiles variable
    """
    def loadConfig(self):
        for prc in configfiles:
            if (os.path.exists(prc)):
                loadPrcFile(prc)

# Run the application
Application().run()

characters/base.py:

'''
    This is an abstract class for a character defining some common methods for any actor(player), be it plyabla or non-playable.
'''

# Import system libraries
import os
import sys
import struct

# Add root path (to use constants)
sys.path.insert(0, '../')

import constants as const
import uuid
from abc import ABC, abstractmethod

from panda3d.core import *
from panda3d.physics import *
from direct.actor.Actor import Actor
from direct.showbase.ShowBase import ShowBase
from direct.interval.IntervalGlobal import Sequence
from direct.interval.ActorInterval import ActorInterval


class BaseCharacter(ABC):

    '''
        The init method adds all necessary data to the object
    '''
    def __init__(self, resources, initialPos):
        # Define the character resources, position and parent world
        self.characterResources     = resources
        self.characterInitialPos    = initialPos

        # Generate the player hash
        self.generatePlayerHash()

        # Start rendering the character
        self.createCharacter()


    '''
        Generates a random hash which will be assigned to the player.
        This hash acts as a unique ID. We need this so the player nodes in the scene graph will not coincide
    '''
    def generatePlayerHash(self):
        self.hash = str(uuid.uuid4())


    '''
        Creates the player model(Actor) along with its animations, and starts looping the idle animations.
    '''
    def createCharacter(self):
        self.character = Actor(
            self.characterResources['model'],
            self.characterResources['animations']
        )
        self.character.loop("idle")
        self.character.reparentTo(base.render)
        self.character.setPos(self.characterInitialPos)


    '''
        Adds character physics
    '''
    def addCharacterPhysics(self):
        # Add a physics node to the player
        self.actorPhysicsNode = NodePath("PlayerPhysicsNode_" + self.hash)
        self.actorPhysicsNode.reparentTo(base.render)
        self.actorNode = ActorNode("PlayerPhysics_" + self.hash)
        self.actorNP = self.actorPhysicsNode.attachNewNode(self.actorNode)
        base.physicsMgr.attachPhysicalNode(self.actorNode)
        self.character.reparentTo(self.actorNP)


    '''
        Adds character collision handler
    '''
    def addCharacterCollisions(self):
        # Add collision nodes and rays
        self.actorColSphere = CollisionSphere(self.character.getPos(), 1)
        self.actorColNode   = CollisionNode('PlayerCollisionNode_' + self.hash)
        self.actorColNode.setFromCollideMask(const.FLOOR_MASK)
        self.actorColNode.setIntoCollideMask(const.MASK_OFF)
        self.actorColNode.addSolid(self.actorColSphere)
        self.actorColNodeNP = self.actorNP.attachNewNode(self.actorColNode)

        # Add a Floor CollisionHandler. This ensures that the character will not fall through the floor
        base.physicsHandler.addCollider(self.actorColNodeNP, self.actorNP)
        base.cTrav.addCollider(self.actorColNodeNP, base.physicsHandler)


    '''
        Returns the Actor for this character
    '''
    def getActor(self):
        return self.character


    '''
        Tries to animate the character. If it cannot, prints a message and passes
    '''
    def animate(self, animation: str, loop: bool):
        try:
            if (loop):
                self.character.loop(animation)
            else:
                self.character.play(animation)

        except Error:
            print("Character " + self.hash + " has no valid animation with the name: " + animation)
            pass


    '''
        Updates the character position according to the pshysics node position
    '''
    def update(self):
        # self.character.setPos(self.actorNP.getPos())
        pass

    '''
        Returns the player's current position
    '''
    def getPos(self):
        return self.character.getPos()


    '''
        Returns the player's current X position
    '''
    def getX(self):
        return self.character.getX()

    '''
        Returns the player's current Y position
    '''
    def getY(self):
        return self.character.getY()


    '''
        Returns the player's current Z position
    '''
    def getZ(self):
        return self.character.getZ()

    '''
        Prints the current actor position and phsyics node position
    '''
    def debug(self):
        print('PLAYER POS: ', self.character.getPos())
        print('PHYS PLAYER POS: ', self.actorNP.getPos())
        print('----------------------------------------------------')

    '''
        @TODO: implement this method here
        This will move the character to a specified position
    '''
    def moveTo(self, position):
        # Look at the position at which we're going
        self.character.headsUp(position)
        # Quick hack - for some models, lookAt and headsUp rotates the wrong way. Rotate 180 degrees
        self.character.setH(self.character.getH()-180)
        moveInterval = self.character.posInterval(3.0, position)
        moveSequence = Sequence(moveInterval)
        moveSequence.start()
        self.animate('run', True)

characters/player.py:

'''
    This class defines a playable character.
    @TODO: Finish class. At the moment there is nothing to extend from the BaseCharacter class.
'''

from . import BaseCharacter

class PlayerCharacter(BaseCharacter):
    pass

constants.py:

'''
    CONSTANTS TO BE USED EVERYWHERE
'''

from panda3d.core import *

FLOOR_MASK  = BitMask32.bit(1)
MASK_OFF    = BitMask32.allOff()

I would really appreciate it if someone would be able to give me a few hints and tips as to what I am doing wrong, and how I could make it work properly.

Thank you in advance.

I’ve never used that way, but seems to me to be better to use a ray.

I’m not too familiar with the Panda physics system, but I think you’d want to move self.actorPhysicsNode instead of self.character. The character is just the model itself and has the ActorNode as its parent, so I think you’d want to move that instead in order to get proper physics.

I was trying to use a sphere so I would be able to detect collisions with other objects aswell, not only the floor. And from what I understand from the documentation, the sphere is the way to go in this case. I just tried with a CollisionRay and it doesn’t help. It falls through the floor endlessly (it doesn’t even stop as it does with the sphere)

I understand. However when I do this, the character model moves weird (not in the direction I click it, but this is another issue). The problem is that the physics node still falls through the floor. :frowning:

Do you have this line in the model file (egg)?

{ Polyset keep descend }

Yes, multiple times, actually. One in the terrain group, and one in each rock and each tree group.

You can use the RPG example: storage.googleapis.com/google-c … rchive.zip

Thanks! I will have a look at it.

I dont understand. Care to explain a bit more please?

On another note, I have managed to make the collision handler working by setting the player’s CollisionSphere position from characterInitialPos to (0, 0, 0). However, there is another issue now: The actor’s model does not seem to move where it should be. The physics node falls and stops as it should, and setting the collision sphere to be visible shows that it is also where it should be, but the character model itself seems to go haywire, in what seems like pseudo-random locations.

I have managed to fix this issue aswell, by setting the character actor(model) position to (0, 0, 0). I should have figured it should be like this, since after reparenting it to the physics node means that its position is relative to the physics node, not to the environment itself.

The collisions still seem to be wonky when colliting into a rock, a tree or a top of a hill (the node seems to go randomly everywhere for a second or so, after which it goes where it should), but I’m sure I will manage to fix that aswell.