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 | The default value is \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)}. The default value is |
58 | \c{(0, 0, 0)}. |
59 | */ |
60 | |
61 | /*! |
62 | \qmlproperty bool CharacterController::midAirControl |
63 | |
64 | This property defines whether the \l movement property has effect when the character is in free |
65 | fall. This is only relevant if \l gravity in not null. A value of \c true means that the |
66 | character will change direction in mid-air when \c movement changes. A value of \c false means that |
67 | the character will continue on its current trajectory until it hits another object. The default |
68 | value is \c true. |
69 | */ |
70 | |
71 | /*! |
72 | \qmlproperty Collisions CharacterController::collisions |
73 | \readonly |
74 | |
75 | This property holds the current collision state of the character. It is either \c None for no |
76 | collision, or an OR combination of \c Side, \c Up, and \c Down: |
77 | |
78 | \value CharacterController.None |
79 | The character is not touching anything. If gravity is non-null, this means that the |
80 | character is in free fall. |
81 | \value CharacterController.Side |
82 | The character is touching something on its side. |
83 | \value CharacterController.Up |
84 | The character is touching something above it. |
85 | \value CharacterController.Down |
86 | The character is touching something below it. In standard gravity, this means |
87 | that the character is on the ground. |
88 | |
89 | \note The directions are defined relative to standard gravity: \c Up is always along the |
90 | positive y-axis, regardless of the value of \l {gravity}{CharacterController.gravity} |
91 | or \l{PhysicsWorld::gravity}{PhysicsWorld.gravity} |
92 | */ |
93 | |
94 | /*! |
95 | \qmlproperty bool CharacterController::enableShapeHitCallback |
96 | \since 6.6 |
97 | |
98 | This property enables/disables the \l {CharacterController::shapeHit} callback for this |
99 | character controller. |
100 | |
101 | Default value: false |
102 | */ |
103 | |
104 | /*! |
105 | \qmlmethod CharacterController::teleport(vector3d position) |
106 | Immediately move the character to \a position without checking for collisions. |
107 | The caller is responsible for avoiding overlap with static objects. |
108 | */ |
109 | |
110 | /*! |
111 | \qmlsignal CharacterController::shapeHit(PhysicsNode *body, vector3D position, vector3D impulse, |
112 | vector3D normal) |
113 | \since 6.6 |
114 | |
115 | This signal is emitted when \l {CharacterController::}{movement} has been |
116 | called and it would result |
117 | in a collision with a \l {DynamicRigidBody} or a \l {StaticRigidBody} and |
118 | \l {CharacterController::} {enableShapeHitCallback} is set to \c true. |
119 | The parameters \a body, \a position, \a impulse and \a normal contain the body, position, |
120 | impulse force and normal for the contact point. |
121 | */ |
122 | |
123 | QCharacterController::QCharacterController() = default; |
124 | |
125 | const QVector3D &QCharacterController::movement() const |
126 | { |
127 | return m_movement; |
128 | } |
129 | |
130 | void QCharacterController::setMovement(const QVector3D &newMovement) |
131 | { |
132 | if (m_movement == newMovement) |
133 | return; |
134 | m_movement = newMovement; |
135 | emit movementChanged(); |
136 | } |
137 | |
138 | const QVector3D &QCharacterController::gravity() const |
139 | { |
140 | return m_gravity; |
141 | } |
142 | |
143 | void QCharacterController::setGravity(const QVector3D &newGravity) |
144 | { |
145 | if (m_gravity == newGravity) |
146 | return; |
147 | m_gravity = newGravity; |
148 | emit gravityChanged(); |
149 | } |
150 | |
151 | // Calculate move based on movement/gravity |
152 | |
153 | QVector3D QCharacterController::getDisplacement(float deltaTime) |
154 | { |
155 | // Start with basic movement, assuming no other factors |
156 | QVector3D displacement = sceneRotation() * m_movement * deltaTime; |
157 | |
158 | // modified based on gravity |
159 | const auto g = m_gravity; |
160 | if (!g.isNull()) { |
161 | |
162 | // Avoid "spider mode": we are also supposed to be in free fall if gravity |
163 | // is pointing away from a surface we are touching. I.e. we are NOT in free |
164 | // fall only if gravity has a component in the direction of one of the collisions. |
165 | // Also: if we have "upwards" free fall velocity, that motion needs to stop |
166 | // when we hit the "ceiling"; i.e we are not in free fall at the moment of impact. |
167 | auto isGrounded = [this](){ |
168 | if (m_collisions == Collision::None) |
169 | return false; |
170 | |
171 | // Standard gravity case first |
172 | if (m_gravity.y() < 0) { |
173 | if (m_collisions & Collision::Down) |
174 | return true; // We land on the ground |
175 | if ((m_collisions & Collision::Up) && m_freeFallVelocity.y() > 0) |
176 | return true; // We bump our head on the way up |
177 | } |
178 | |
179 | // Inverse gravity next: exactly the opposite |
180 | if (m_gravity.y() > 0) { |
181 | if (m_collisions & Collision::Up) |
182 | return true; |
183 | if ((m_collisions & Collision::Down) && m_freeFallVelocity.y() < 0) |
184 | return true; |
185 | } |
186 | |
187 | // The sideways gravity case can't be perfectly handled since we don't |
188 | // know the direction of sideway contacts. We could in theory inspect |
189 | // the mesh, but that is far too complex for an extremely marginal use case. |
190 | |
191 | if ((m_gravity.x() != 0 || m_gravity.z() != 0) && m_collisions & Collision::Side) |
192 | return true; |
193 | |
194 | return false; |
195 | }; |
196 | |
197 | bool freeFalling = !isGrounded(); |
198 | if (freeFalling) { |
199 | if (!m_midAirControl) |
200 | displacement = {}; // Ignore the movement() controls in true free fall |
201 | |
202 | displacement += m_freeFallVelocity * deltaTime; |
203 | m_freeFallVelocity += g * deltaTime; |
204 | } else { |
205 | m_freeFallVelocity = displacement / deltaTime + g * deltaTime; |
206 | if (m_midAirControl) // free fall only straight down |
207 | m_freeFallVelocity = |
208 | QVector3D::dotProduct(v1: m_freeFallVelocity, v2: g.normalized()) * g.normalized(); |
209 | } |
210 | const QVector3D gravityAcceleration = 0.5 * deltaTime * deltaTime * g; |
211 | displacement += gravityAcceleration; // always add gravitational acceleration, in case we start |
212 | // to fall. If we don't, PhysX will move us back to the ground. |
213 | } |
214 | |
215 | return displacement; |
216 | } |
217 | |
218 | bool QCharacterController::midAirControl() const |
219 | { |
220 | return m_midAirControl; |
221 | } |
222 | |
223 | void QCharacterController::setMidAirControl(bool newMidAirControl) |
224 | { |
225 | if (m_midAirControl == newMidAirControl) |
226 | return; |
227 | m_midAirControl = newMidAirControl; |
228 | emit midAirControlChanged(); |
229 | } |
230 | |
231 | void QCharacterController::teleport(const QVector3D &position) |
232 | { |
233 | m_teleport = true; |
234 | m_teleportPosition = position; |
235 | m_freeFallVelocity = {}; |
236 | } |
237 | |
238 | bool QCharacterController::getTeleport(QVector3D &position) |
239 | { |
240 | if (m_teleport) { |
241 | position = m_teleportPosition; |
242 | m_teleport = false; |
243 | return true; |
244 | } |
245 | return false; |
246 | } |
247 | |
248 | const QCharacterController::Collisions &QCharacterController::collisions() const |
249 | { |
250 | return m_collisions; |
251 | } |
252 | |
253 | void QCharacterController::setCollisions(const Collisions &newCollisions) |
254 | { |
255 | if (m_collisions == newCollisions) |
256 | return; |
257 | m_collisions = newCollisions; |
258 | emit collisionsChanged(); |
259 | } |
260 | |
261 | bool QCharacterController::enableShapeHitCallback() const |
262 | { |
263 | return m_enableShapeHitCallback; |
264 | } |
265 | |
266 | QAbstractPhysXNode *QCharacterController::createPhysXBackend() |
267 | { |
268 | return new QPhysXCharacterController(this); |
269 | } |
270 | |
271 | void QCharacterController::setEnableShapeHitCallback(bool newEnableShapeHitCallback) |
272 | { |
273 | if (m_enableShapeHitCallback == newEnableShapeHitCallback) |
274 | return; |
275 | m_enableShapeHitCallback = newEnableShapeHitCallback; |
276 | emit enableShapeHitCallbackChanged(); |
277 | } |
278 | |
279 | QT_END_NAMESPACE |
280 | |