| 1 | // Copyright (C) 2022 The Qt Company Ltd. |
| 2 | // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only |
| 3 | |
| 4 | #include "qcharactercontroller_p.h" |
| 5 | |
| 6 | #include "physxnode/qphysxcharactercontroller_p.h" |
| 7 | |
| 8 | QT_BEGIN_NAMESPACE |
| 9 | |
| 10 | /*! |
| 11 | \qmltype CharacterController |
| 12 | \inqmlmodule QtQuick3D.Physics |
| 13 | \inherits PhysicsBody |
| 14 | \since 6.4 |
| 15 | \brief Controls the motion of a character. |
| 16 | |
| 17 | The CharacterController type controls the motion of a character. |
| 18 | |
| 19 | A character is an entity that moves under external control, but is still constrained |
| 20 | by physical barriers and (optionally) subject to gravity. This is in contrast to |
| 21 | \l{DynamicRigidBody}{dynamic rigid bodies} which are either completely controlled by |
| 22 | the physics simulation (for non-kinematic bodies); or move exactly where placed, |
| 23 | regardless of barriers (for kinematic objects). |
| 24 | |
| 25 | To control the motion of a character controller, set \l movement to the desired velocity. |
| 26 | |
| 27 | For a first-person view, the camera is typically placed inside a character controller. |
| 28 | |
| 29 | \note \l {PhysicsNode::collisionShapes}{collisionShapes} must be set to |
| 30 | a single \l {CapsuleShape}. No other shapes are supported. |
| 31 | |
| 32 | \note The character controller is able to scale obstacles that are lower than one fourth of |
| 33 | the capsule shape's height. |
| 34 | |
| 35 | \sa {Qt Quick 3D Physics Shapes and Bodies}{Shapes and Bodies overview documentation} |
| 36 | */ |
| 37 | |
| 38 | /*! |
| 39 | \qmlproperty vector3d CharacterController::movement |
| 40 | |
| 41 | This property defines the controlled motion of the character. This is the velocity the character |
| 42 | would move in the absence of gravity and without interacting with other physics objects. |
| 43 | |
| 44 | This property does not reflect the actual velocity of the character. If the character is stuck |
| 45 | against terrain, the character can move slower than the speed defined by \c movement. Conversely, if the |
| 46 | character is in free fall, it may move much faster. |
| 47 | |
| 48 | Default value: \c{(0, 0, 0)} |
| 49 | */ |
| 50 | |
| 51 | /*! |
| 52 | \qmlproperty vector3d CharacterController::gravity |
| 53 | |
| 54 | This property defines the gravitational acceleration that applies to the character. |
| 55 | For a character that walks on the ground, it should typically be set to |
| 56 | \l{PhysicsWorld::gravity}{PhysicsWorld.gravity}. A floating character that has movement |
| 57 | controls in three dimensions will normally have gravity \c{(0, 0, 0)}. |
| 58 | |
| 59 | Default value: \c{(0, 0, 0)}. |
| 60 | */ |
| 61 | |
| 62 | /*! |
| 63 | \qmlproperty bool CharacterController::midAirControl |
| 64 | |
| 65 | This property defines whether the \l movement property has effect when the character is in free |
| 66 | fall. This is only relevant if \l gravity in not null. A value of \c true means that the |
| 67 | character will change direction in mid-air when \c movement changes. A value of \c false means that |
| 68 | the character will continue on its current trajectory until it hits another object. |
| 69 | |
| 70 | Default value: \c true |
| 71 | */ |
| 72 | |
| 73 | /*! |
| 74 | \qmlproperty Collisions CharacterController::collisions |
| 75 | \readonly |
| 76 | |
| 77 | This property holds the current collision state of the character. It is either \c None for no |
| 78 | collision, or an OR combination of \c Side, \c Up, and \c Down: |
| 79 | |
| 80 | \value CharacterController.None |
| 81 | The character is not touching anything. If gravity is non-null, this means that the |
| 82 | character is in free fall. |
| 83 | \value CharacterController.Side |
| 84 | The character is touching something on its side. |
| 85 | \value CharacterController.Up |
| 86 | The character is touching something above it. |
| 87 | \value CharacterController.Down |
| 88 | The character is touching something below it. In standard gravity, this means |
| 89 | that the character is on the ground. |
| 90 | |
| 91 | \note The directions are defined relative to standard gravity: \c Up is always along the |
| 92 | positive y-axis, regardless of the value of \l {gravity}{CharacterController.gravity} |
| 93 | or \l{PhysicsWorld::gravity}{PhysicsWorld.gravity} |
| 94 | */ |
| 95 | |
| 96 | /*! |
| 97 | \qmlproperty bool CharacterController::enableShapeHitCallback |
| 98 | \since 6.6 |
| 99 | |
| 100 | This property enables/disables the \l {CharacterController::shapeHit} callback for this |
| 101 | character controller. |
| 102 | |
| 103 | Default value: \c{false} |
| 104 | */ |
| 105 | |
| 106 | /*! |
| 107 | \qmlmethod CharacterController::teleport(vector3d position) |
| 108 | Immediately move the character to \a position without checking for collisions. |
| 109 | The caller is responsible for avoiding overlap with static objects. |
| 110 | */ |
| 111 | |
| 112 | /*! |
| 113 | \qmlsignal CharacterController::shapeHit(PhysicsNode *body, vector3D position, vector3D impulse, |
| 114 | vector3D normal) |
| 115 | \since 6.6 |
| 116 | |
| 117 | This signal is emitted when \l {CharacterController::}{movement} has been |
| 118 | called and it would result |
| 119 | in a collision with a \l {DynamicRigidBody} or a \l {StaticRigidBody} and |
| 120 | \l {CharacterController::} {enableShapeHitCallback} is set to \c true. |
| 121 | The parameters \a body, \a position, \a impulse and \a normal contain the body, position, |
| 122 | impulse force and normal for the contact point. |
| 123 | */ |
| 124 | |
| 125 | QCharacterController::QCharacterController() = default; |
| 126 | |
| 127 | const QVector3D &QCharacterController::movement() const |
| 128 | { |
| 129 | return m_movement; |
| 130 | } |
| 131 | |
| 132 | void QCharacterController::setMovement(const QVector3D &newMovement) |
| 133 | { |
| 134 | if (m_movement == newMovement) |
| 135 | return; |
| 136 | m_movement = newMovement; |
| 137 | emit movementChanged(); |
| 138 | } |
| 139 | |
| 140 | const QVector3D &QCharacterController::gravity() const |
| 141 | { |
| 142 | return m_gravity; |
| 143 | } |
| 144 | |
| 145 | void QCharacterController::setGravity(const QVector3D &newGravity) |
| 146 | { |
| 147 | if (m_gravity == newGravity) |
| 148 | return; |
| 149 | m_gravity = newGravity; |
| 150 | emit gravityChanged(); |
| 151 | } |
| 152 | |
| 153 | // Calculate move based on movement/gravity |
| 154 | |
| 155 | QVector3D QCharacterController::getDisplacement(float deltaTime) |
| 156 | { |
| 157 | // Start with basic movement, assuming no other factors |
| 158 | QVector3D displacement = sceneRotation() * m_movement * deltaTime; |
| 159 | |
| 160 | // modified based on gravity |
| 161 | const auto g = m_gravity; |
| 162 | if (!g.isNull()) { |
| 163 | |
| 164 | // Avoid "spider mode": we are also supposed to be in free fall if gravity |
| 165 | // is pointing away from a surface we are touching. I.e. we are NOT in free |
| 166 | // fall only if gravity has a component in the direction of one of the collisions. |
| 167 | // Also: if we have "upwards" free fall velocity, that motion needs to stop |
| 168 | // when we hit the "ceiling"; i.e we are not in free fall at the moment of impact. |
| 169 | auto isGrounded = [this](){ |
| 170 | if (m_collisions == Collision::None) |
| 171 | return false; |
| 172 | |
| 173 | // Standard gravity case first |
| 174 | if (m_gravity.y() < 0) { |
| 175 | if (m_collisions & Collision::Down) |
| 176 | return true; // We land on the ground |
| 177 | if ((m_collisions & Collision::Up) && m_freeFallVelocity.y() > 0) |
| 178 | return true; // We bump our head on the way up |
| 179 | } |
| 180 | |
| 181 | // Inverse gravity next: exactly the opposite |
| 182 | if (m_gravity.y() > 0) { |
| 183 | if (m_collisions & Collision::Up) |
| 184 | return true; |
| 185 | if ((m_collisions & Collision::Down) && m_freeFallVelocity.y() < 0) |
| 186 | return true; |
| 187 | } |
| 188 | |
| 189 | // The sideways gravity case can't be perfectly handled since we don't |
| 190 | // know the direction of sideway contacts. We could in theory inspect |
| 191 | // the mesh, but that is far too complex for an extremely marginal use case. |
| 192 | |
| 193 | if ((m_gravity.x() != 0 || m_gravity.z() != 0) && m_collisions & Collision::Side) |
| 194 | return true; |
| 195 | |
| 196 | return false; |
| 197 | }; |
| 198 | |
| 199 | bool freeFalling = !isGrounded(); |
| 200 | if (freeFalling) { |
| 201 | if (!m_midAirControl) |
| 202 | displacement = {}; // Ignore the movement() controls in true free fall |
| 203 | |
| 204 | displacement += m_freeFallVelocity * deltaTime; |
| 205 | m_freeFallVelocity += g * deltaTime; |
| 206 | } else { |
| 207 | m_freeFallVelocity = displacement / deltaTime + g * deltaTime; |
| 208 | if (m_midAirControl) // free fall only straight down |
| 209 | m_freeFallVelocity = |
| 210 | QVector3D::dotProduct(v1: m_freeFallVelocity, v2: g.normalized()) * g.normalized(); |
| 211 | } |
| 212 | const QVector3D gravityAcceleration = 0.5 * deltaTime * deltaTime * g; |
| 213 | displacement += gravityAcceleration; // always add gravitational acceleration, in case we start |
| 214 | // to fall. If we don't, PhysX will move us back to the ground. |
| 215 | } |
| 216 | |
| 217 | return displacement; |
| 218 | } |
| 219 | |
| 220 | bool QCharacterController::midAirControl() const |
| 221 | { |
| 222 | return m_midAirControl; |
| 223 | } |
| 224 | |
| 225 | void QCharacterController::setMidAirControl(bool newMidAirControl) |
| 226 | { |
| 227 | if (m_midAirControl == newMidAirControl) |
| 228 | return; |
| 229 | m_midAirControl = newMidAirControl; |
| 230 | emit midAirControlChanged(); |
| 231 | } |
| 232 | |
| 233 | void QCharacterController::teleport(const QVector3D &position) |
| 234 | { |
| 235 | m_teleport = true; |
| 236 | m_teleportPosition = position; |
| 237 | m_freeFallVelocity = {}; |
| 238 | } |
| 239 | |
| 240 | bool QCharacterController::getTeleport(QVector3D &position) |
| 241 | { |
| 242 | if (m_teleport) { |
| 243 | position = m_teleportPosition; |
| 244 | m_teleport = false; |
| 245 | return true; |
| 246 | } |
| 247 | return false; |
| 248 | } |
| 249 | |
| 250 | const QCharacterController::Collisions &QCharacterController::collisions() const |
| 251 | { |
| 252 | return m_collisions; |
| 253 | } |
| 254 | |
| 255 | void QCharacterController::setCollisions(const Collisions &newCollisions) |
| 256 | { |
| 257 | if (m_collisions == newCollisions) |
| 258 | return; |
| 259 | m_collisions = newCollisions; |
| 260 | emit collisionsChanged(); |
| 261 | } |
| 262 | |
| 263 | bool QCharacterController::enableShapeHitCallback() const |
| 264 | { |
| 265 | return m_enableShapeHitCallback; |
| 266 | } |
| 267 | |
| 268 | QAbstractPhysXNode *QCharacterController::createPhysXBackend() |
| 269 | { |
| 270 | return new QPhysXCharacterController(this); |
| 271 | } |
| 272 | |
| 273 | void QCharacterController::setEnableShapeHitCallback(bool newEnableShapeHitCallback) |
| 274 | { |
| 275 | if (m_enableShapeHitCallback == newEnableShapeHitCallback) |
| 276 | return; |
| 277 | m_enableShapeHitCallback = newEnableShapeHitCallback; |
| 278 | emit enableShapeHitCallbackChanged(); |
| 279 | } |
| 280 | |
| 281 | QT_END_NAMESPACE |
| 282 | |