Hello, I have a problem with bullet physics. I create collisions on my terrain using a script that retrieves the vertices of triangles to create collision faces with Triangle Mesh Shape.
The problem is that my player is subject to gravity, so if he doesn’t have enough speed and walks on the edge of a face on the ground, he gets stuck.
Are you using physics to control your player-character?
If so, that can be finicky to get working reliably, I think.
Generally speaking, I think that it’s often advised that characters instead be controlled via “kinematic” means–essentially, not handled via physics, but instead via game-logic in response to collisions. This allows one to design a character that responds in a simpler, more-intuitive, and more-controlled manner.
There is my player class
class Player:
# Constantes de configuration
DEFAULT_RUN_SPEED = 3.0
DEFAULT_WALK_SPEED = 1.5
CROUCH_RUN_SPEED = 0.5
CROUCH_WALK_SPEED = 0.1
STAMINA_LOW_THRESHOLD = 20
STAMINA_CRITICAL_THRESHOLD = 10
TELEPORT_HEIGHT = -50
TELEPORT_TARGET_Z = 200
SENSIBILITY = 0.1
FOV = 120
JUMP_HEIGHT = 0.5
JUMP_SPEED = 4.0
STAMINA_DRAIN_RATE = 2.5
STAMINA_REGEN_STILL = 7.5
STAMINA_REGEN_MOVING = 5.0
CAMERA_SHAKE_INTENSITY_MULTIPLIER = 0.005
MODEL_HEIGHT_OFFSET = 1.773 / 2
CROUCH_MODEL_Z = -1.8
CROUCH_CONTROLLER_Z_OFFSET = -0.4
STAND_MODEL_Z = -1.773 / 2
def __init__(self, parentClass=None, model='models/playertest.bam'):
"""
Initialise le joueur avec ses paramètres et composants.
Args:
parentClass: L'instance principale de l'application (ShowBase).
model: Chemin vers le modèle du joueur.
"""
# état
self.parentClass = parentClass
self.ui = GUI()
self.knockback_vector = Vec3(0, 0, 0)
self.knockback_time = 0.0
self.iscrounching = False
self.dead_state = False
self.camera_up = False
# paramètres mouvement / caméra
self.runspeed = self.DEFAULT_RUN_SPEED
self.walkspeed = self.DEFAULT_WALK_SPEED
self.stamina = 100
self.hp = 100
self.flou = 1
self.sensibility = max(0.01, self.SENSIBILITY) # Validation pour éviter 0
self.mouse_center = (self.parentClass.win.getXSize() // 2, self.parentClass.win.getYSize() // 2)
self.jumping = False
# Bullet world
self.bullet_world = self.parentClass.bullet_world
self.bullet_world.setGravity(Vec3(0, 0, -9.81))
# formes (debout / accroupi)
self.shape = BulletCapsuleShape(0.4/2, 1.773 - 2*0.4/2, ZUp) # debout
# character controller (une seule création ici)
self.controller = BulletCharacterControllerNode(self.shape, 0.4, 'Player')
self.controller_np = self.parentClass.render.attachNewNode(self.controller)
self.controller_np.setPos(0, 0, 1)
self.bullet_world.attachCharacter(self.controller)
# modèle visuel (après création du controller)
self.character = Character(self.parentClass)
self.model = self.character.actor
self.model.reparentTo(self.controller_np)
self.model.setH(0)
self.model.setZ(0 - 1.773/2) # ajuster la hauteur du modèle par rapport au controller
# caméra
self.head_np = self.model.exposeJoint(None, "modelRoot", "mixamorig:Head")
# Node intermédiaire pour corriger le roll
self.camera_roll_correction = self.head_np.attachNewNode("camera_roll_correction")
self.camera_roll_correction.setHpr(0, 0, 0) # Roll, pitch, yaw neutres
self.camera_z = 10
self.camera_roll_correction.setPos(-10, -15, self.camera_z) # recul + hauteur caméra
# Lens
self.parentClass.camLens.setFov(120)
self.parentClass.camLens.setNearFar(0.1, 150)
# contrôle du cou
self.neck_np = self.model.controlJoint(None, "modelRoot", "mixamorig:Neck")
self.neck_np.setR(0) # verrouiller le roll sur le cou
self.turning = False # état de rotation forcée
self.moving = False # état de mouvement forcé
self.raycast = PlayerRaycast(
render=self.parentClass.render,
camera=self.parentClass.camera,
bullet_world=self.parentClass.bullet_world,
max_distance=2.5,
)
# input
self.controls = {
"z": "forward",
"s": "backward",
"q": "left",
"d": "right",
"c": "crouch",
"shift": "run",
"space": "jump"
}
self.parentClass.accept("e", self.try_interact)
self.parentClass.accept("e-repeat", self.try_interact)
self.parentClass.keyMap = {action: False for action in self.controls.values()}
for key, action in self.controls.items():
self.parentClass.accept(key, self.setKey, [action, True])
self.parentClass.accept(f"{key}-up", self.setKey, [action, False])
# HUD / lastpos / filters
self.lastpos = self.controller_np.getPos()
self.fov = 120
# tasks
self.parentClass.taskMgr.add(self.update_player, "update_player")
self.parentClass.taskMgr.add(self.param_stamina, "param_stamina")
self.parentClass.taskMgr.add(self.update_camera, "update_camera")
# à la fin du __init__
self.parentClass.taskMgr.doMethodLater(0.1, self.init_filters, "init_filters")
self.parentClass.taskMgr.add(self.raycast.update_debug_smoothly, "update_raycast_debug")
def try_interact(self):
"""Tente une interaction avec l'environnement via le raycast."""
if self.raycast.interact():
print("[Player: Raycast] Interaction réussie")
else:
print("[Player: Raycast] Rien à interagir")
def setKey(self, key, value):
"""Met à jour l'état d'une touche dans keyMap."""
self.parentClass.keyMap[key] = value
def jump(self):
"""Fait sauter le joueur si possible."""
if not self.jumping and self.controller.isOnGround():
self.controller.setMaxJumpHeight(self.JUMP_HEIGHT)
self.controller.setJumpSpeed(self.JUMP_SPEED)
self.controller.doJump()
self.jumping = True
def update_player(self, task):
dt = globalClock.getDt()
mw = self.parentClass.mouseWatcherNode
# Gestion de la mort
self.handle_death()
# Téléport si tombé
self.handle_teleport()
# Knockback
if self.handle_knockback(dt):
return Task.cont
# Input mouvement et animations
input_vector = self.handle_movement_input(mw)
# Crouch
self.handle_crouch(mw)
# Jump
self.handle_jump(mw)
# Calcul et application du mouvement
self.apply_movement(input_vector, mw, dt)
return Task.cont
def handle_death(self):
"""Gère la logique de mort du joueur."""
if self.hp <= 0 and not self.dead_state:
if hasattr(self.parentClass, 'terrain') and hasattr(self.parentClass.terrain, "batterie_3d"):
musique = self.parentClass.terrain.batterie_3d
new_time = max(0, musique.getTime() - 0.4)
self.dead_state = True
for i in range(20):
musique.setTime(new_time)
musique.play()
sleep(0.3)
self.parentClass.quit_game()
else:
print("[Player] Erreur : musique non disponible, arrêt du jeu.")
self.parentClass.quit_game()
def handle_teleport(self):
"""Téléporte le joueur s'il est tombé trop bas."""
if self.controller_np.getZ() < self.TELEPORT_HEIGHT:
target_pos = Vec3(self.controller_np.getX(), self.controller_np.getY(), self.TELEPORT_TARGET_Z)
h, p, r = self.controller_np.getH(), self.controller_np.getP(), self.controller_np.getR()
try:
self.bullet_world.removeCharacter(self.controller)
except Exception as e:
print(f"[Player] Erreur lors de la suppression du controller : {e}")
self.controller_np.setPos(target_pos)
self.controller_np.setHpr(h, p, r)
self.bullet_world.attachCharacter(self.controller)
try:
self.controller.setLinearMovement(Vec3(0, 0, 0), True)
except Exception as e:
print(f"[Player] Erreur lors de l'arrêt du mouvement : {e}")
def handle_knockback(self, dt):
"""Gère le knockback. Retourne True si knockback actif."""
if self.knockback_time > 0:
self.controller.setLinearMovement(self.knockback_vector, True)
self.knockback_time -= dt
return True
return False
def handle_movement_input(self, mw):
"""Gère les inputs de mouvement et met à jour les animations."""
input_vector = Vec3(0, 0, 0)
if mw.is_button_down(KeyboardButton.ascii_key("z")):
input_vector.y += 1
if mw.is_button_down(KeyboardButton.ascii_key("s")):
input_vector.y -= 1
if mw.is_button_down(KeyboardButton.ascii_key("q")):
input_vector.x -= 1
if mw.is_button_down(KeyboardButton.ascii_key("d")):
input_vector.x += 1
if input_vector.length_squared() > 0:
if mw.is_button_down(KeyboardButton.lshift()) and self.stamina > 0:
if self.character.state != 'Run':
self.character.request('Run')
else:
if self.character.state != 'Walk':
self.character.request('Walk')
self.moving = True
else:
if self.character.state != 'Idle':
self.character.request('Idle')
self.moving = False
return input_vector
def handle_crouch(self, mw):
"""Gère l'accroupissement."""
if mw.is_button_down(KeyboardButton.ascii_key("c")):
if not self.iscrounching and not self.jumping:
self._switch_to_crouch()
else:
if self.iscrounching:
self._switch_to_stand()
def _switch_to_crouch(self):
"""Passe en mode accroupi en changeant la forme du controller."""
if not self.iscrounching:
sz = 0.6
self.controller.getShape().setScale(Vec3(1, 1, sz))
self.controller_np.setScale(Vec3(1, 1, sz))
self.controller_np.setZ(self.controller_np.getZ() - 0.4)
self.model.setZ(self.CROUCH_MODEL_Z)
self.runspeed = self.CROUCH_RUN_SPEED
self.walkspeed = self.CROUCH_WALK_SPEED
self.iscrounching = True
def _switch_to_stand(self):
"""Passe en mode debout en changeant la forme du controller."""
if self.iscrounching:
sz = 1.0
self.controller.getShape().setScale(Vec3(1, 1, sz))
self.controller_np.setScale(Vec3(1, 1, sz))
self.controller_np.setZ(self.controller_np.getZ() + 0.4)
self.model.setZ(self.STAND_MODEL_Z)
self.runspeed = self.DEFAULT_RUN_SPEED
self.walkspeed = self.DEFAULT_WALK_SPEED
self.iscrounching = False
def handle_jump(self, mw):
"""Gère le saut."""
if mw.is_button_down(KeyboardButton.space()):
self.jump()
if self.controller.isOnGround():
self.jumping = False
def apply_movement(self, input_vector, mw, dt):
"""Calcule et applique le mouvement."""
speed = self.runspeed if mw.is_button_down(KeyboardButton.lshift()) else self.walkspeed
if input_vector.length_squared() > 0:
input_vector.normalize()
input_vector *= speed
input_vector = self.model.getQuat(self.parentClass.render).xform(input_vector)
try:
self.controller.setLinearMovement(input_vector, True)
except Exception:
self.controller_np.setPos(self.controller_np.getPos() + input_vector * dt)
def param_stamina(self, task):
"""
Met à jour la stamina, ajuste les vitesses, applique le shake caméra et met à jour l'UI.
"""
dt = globalClock.getDt()
mw = self.parentClass.mouseWatcherNode
# Calcul déplacement
current_pos = self.controller_np.getPos()
move_speed = (current_pos - self.lastpos).length() / max(dt, 1e-6)
self.lastpos = current_pos
# RÉDUCTION / regen STAMINA
if mw.is_button_down(KeyboardButton.lshift()) and move_speed > 0:
self.stamina = max(0, self.stamina - self.STAMINA_DRAIN_RATE * dt)
else:
regen = self.STAMINA_REGEN_STILL if move_speed == 0 else self.STAMINA_REGEN_MOVING
self.stamina = min(100, self.stamina + regen * dt)
# AJUSTEMENT VITESSE
if self.stamina < self.STAMINA_LOW_THRESHOLD:
self.runspeed = 1
self.walkspeed = 0.833
else:
self.runspeed = self.DEFAULT_RUN_SPEED
self.walkspeed = self.DEFAULT_WALK_SPEED
# SHAKE CAM SI PEU DE STAMINA
if self.stamina < self.STAMINA_CRITICAL_THRESHOLD:
shake_intensity = (self.STAMINA_CRITICAL_THRESHOLD - self.stamina) * self.CAMERA_SHAKE_INTENSITY_MULTIPLIER
self.parentClass.camera.setX(
self.parentClass.camera,
shake_intensity * (-1 if globalClock.getFrameCount() % 2 == 0 else 1),
)
# MISE À JOUR BARRES UI
stamina_ratio = self.stamina / 100
self.ui.stamina_bar['frameSize'] = (0.0, 0.8 * stamina_ratio, 0.05, 0.1)
self.ui.stamina_bar['text'] = f"{int(self.stamina)}%"
if hasattr(self, 'hp'):
hp_ratio = self.hp / 100
self.ui.hp_bar['frameSize'] = (0.0, 0.8 * hp_ratio, 0.0, 0.05)
self.ui.hp_bar['text'] = f"{int(self.hp)}%"
return Task.cont
def update_camera(self, task):
dt = globalClock.getDt()
if self.parentClass.mouseWatcherNode.hasMouse():
md = self.parentClass.win.getPointer(0)
x, y = md.getX(), md.getY()
self.parentClass.win.movePointer(0, *self.mouse_center)
dx = (self.mouse_center[0] - x) * self.sensibility
dy = (self.mouse_center[1] - y) * self.sensibility
# INPUT SUR LE COU (LOCAL AU CORPS)
self.neck_np.setH(self.neck_np.getH() + dx)
new_pitch = self.neck_np.getP() - dy
self.neck_np.setP(max(-75, min(60, new_pitch)))
# ===== DIFF COU / CORPS (LOCAL !) =====
diff = self.neck_np.getH()
max_angle = 15
follow_speed = 30
max_turn_speed = 360 # deg/s
if abs(diff) > max_angle:
# quantité à rattraper
excess = diff - max_angle if diff > 0 else diff + max_angle
turn = excess * follow_speed * dt
turn = max(-max_turn_speed * dt, min(max_turn_speed * dt, turn))
# le corps tourne
self.model.setH(self.model, turn)
# le cou garde une rotation cohérente
self.neck_np.setH(diff - turn)
if abs(diff) > 180:
corrige_turn = self.neck_np.getH()
self.model.setH(corrige_turn)
# ===== CAMÉRA =====
cam_pos = self.camera_roll_correction.getPos(self.parentClass.render)
cam_h = self.camera_roll_correction.getH(self.parentClass.render) + 157.5
cam_p = -self.camera_roll_correction.getP(self.parentClass.render)
self.parentClass.camera.setPos(cam_pos)
self.parentClass.camera.setHpr(cam_h, cam_p, 0)
if self.moving:
#self.model.setH(render, self.parentClass.camera.getH())
pass
return Task.cont
def init_filters(self, task):
if self.parentClass.camNode and self.parentClass.camNode.getDisplayRegion(0):
#self.filters = CommonFilters(self.parentClass.win, self.parentClass.cam)
#self.filters.setBlurSharpen(1)
return Task.done
else:
print("Caméra non encore prête, filtres désactivés.")
self.filters = None
return Task.cont
If I have a good memory, then to get around this problem, I need to turn off the friction when pressing keys, provided that the speed does not change.
Logic: I pressed the forward key, but the speed does not match the expected one, for example, 5 km per hour. So I’m disabling the friction on the Bullet Character Controller.
How i disable my bullet character controller friction?
Since I implemented my controller, I have deleted the old implementations. It’s hard to remember now.
You need to look at the methods of the world class, since it contains links to BulletRigidBodyNode, and extract the character node back, sort of.
Okay, based on a very quick look–I’m tired, and you gave me a lot of code to skim through there–it looks like you’re using the character-controller that comes with Bullet. Which, according to the manual, should be kinematic–so that shouldn’t be the problem.
That said, the manual does also note that said character-controller is at an early stage of development, and may have issues, in particular with dynamic bodies.
Still, if I’m correct in understanding that the triangle mesh that you use for the ground is static–both in the sense of not being a moving object and in the sense of not being altered while the player moves–then I’d expect the character-controller to work well enough.
Let me ask: How small are your polygons? Depending on how the developers of Bullet implemented their character-controller, I could perhaps see small polygons being a problem for it, and larger polygons being less troublesome?
Otherwise, it might be worth considering either checking the Bullet forum for others who may have had similar issues–or making a character-controller of your own. The latter would at least allow you to test, debug, and tweak the character-controller–but could of course be more work overall.
Here is my script that creates triangles taken from another discussion and modified with AI.
def make_collision_from_model(self, input_model, world, excluded_materials=None):
"""
Crée une collision Bullet (BulletTriangleMeshShape) pour un modèle,
en parcourant les GeomNodes et en excluant par matériau.
NOTE D'OPTIMISATION: Cette fonction est dupliquée dans Porte.
Elle devrait être factorisée dans une fonction utilitaire statique.
"""
excluded_materials = excluded_materials or []
mesh = BulletTriangleMesh()
# Parcourt tous les GeomNodes du modèle
for np in input_model.findAllMatches('**/+GeomNode'):
geom_node = np.node()
# Transformation nette pour positionner correctement la géométrie dans le mesh
transform = np.getNetTransform().getMat()
for i in range(geom_node.getNumGeoms()):
state = geom_node.getGeomState(i)
attrib = state.getAttrib(MaterialAttrib)
mat_name = ""
# Récupération du nom du matériau
if attrib:
material = attrib.getMaterial()
if material:
mat_name = material.getName()
# Exclusion par matériau (case-insensitive)
if any(excl.lower() in mat_name.lower() for excl in excluded_materials):
continue
# Ajout de la géométrie au mesh de collision
geom = geom_node.getGeom(i)
mesh.addGeom(geom, transform)
if mesh.getNumTriangles() == 0:
print("[TERRAIN] Aucun triangle de collision trouvé.")
return None
# Crée la forme et le corps Bullet
tri_shape = BulletTriangleMeshShape(mesh, dynamic=False)
body = BulletRigidBodyNode('terrain_collision')
body.addShape(tri_shape)
body.setMass(0) # Mass 0 = statique
body_np = self.parent.render.attachNewNode(body)
body_np.setCollideMask(BitMask32.allOn()) # Masque de collision actif
world.attachRigidBody(body)
return body_np
And it is called like this:
self.make_collision_from_model(self.terrain, self.bullet_world, ["Balloon3", "balloon1", "Balloon6.014", "glass", "matlight", "Matériau.003", "Matériau.006", "roof"])
This looks like a modified version of the make_collision_from_model() function I made for demo programs. The main difference seems to be your usage of a “transform” which you use in addGeom(). Without actually running your program, I’d probably look there first.
I haven’t seen any issues using a Bullet character controller over arbitrary trimeshes, as long as the trimesh is well constructed. If your character is getting “stuck” on edges, there’s probably an issue with the model file in terms of how the geometry is constructed, IE not flat enough, or with vertical bits that stop the CC from traversal.
Then perhaps it’s little wonder if the method is causing issues. :/
Actually, this code was modified by ai a long time ago, and I haven’t modified it in a year.
Sure, but the changes that it made may still have caused your issues here, since it presumably only recently came into contact with the character controller.
Either way, AI “coding” is, I think, unwise: if you don’t know what it’s doing, it’s hard to debug; if you do know what it’s doing then… why not just do it yourself?
And that’s not to mention that these things are by-and-large built on plagiarism–code fed into the training of the things without asking the developers who wrote it.
Don’t use Generative AI.
It’s true that AI makes a lot of mistakes, but I make just as many, especially since I taught myself Python two years ago and I’m still in the beginning of high school, so I haven’t studied programming yet. That’s why I use AI to generate code, especially when I don’t know how to write a complicated program for my level or when I don’t understand the documentation.
To get moving in search of a problem, you can enable debugging mode for Bullet. Evaluate the visually generated surface from the model.
Sure, but you can learn. With time and practice, you will likely increase in skill.
And conversely, relying on AI will likely reduce the rate at which you learn, as you don’t get that practice from which learning arises, nor have to consider the answer.
It’s a bit like walking: the more you walk, the further you can walk; the more you rely on tools that allow you to avoid walking, the less you’ll gain that range of walking.
(Not to mention that, when you make mistakes, you at least may know what you were thinking with your mistaken attempt, from which again you might learn, and which may help you to find the problem. With a mistake produced by AI, you won’t have that understanding, and thus again may learn less, and have more trouble finding the issue.)