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.