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 | |