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
8QT_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
125QCharacterController::QCharacterController() = default;
126
127const QVector3D &QCharacterController::movement() const
128{
129 return m_movement;
130}
131
132void QCharacterController::setMovement(const QVector3D &newMovement)
133{
134 if (m_movement == newMovement)
135 return;
136 m_movement = newMovement;
137 emit movementChanged();
138}
139
140const QVector3D &QCharacterController::gravity() const
141{
142 return m_gravity;
143}
144
145void 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
155QVector3D 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
220bool QCharacterController::midAirControl() const
221{
222 return m_midAirControl;
223}
224
225void QCharacterController::setMidAirControl(bool newMidAirControl)
226{
227 if (m_midAirControl == newMidAirControl)
228 return;
229 m_midAirControl = newMidAirControl;
230 emit midAirControlChanged();
231}
232
233void QCharacterController::teleport(const QVector3D &position)
234{
235 m_teleport = true;
236 m_teleportPosition = position;
237 m_freeFallVelocity = {};
238}
239
240bool 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
250const QCharacterController::Collisions &QCharacterController::collisions() const
251{
252 return m_collisions;
253}
254
255void QCharacterController::setCollisions(const Collisions &newCollisions)
256{
257 if (m_collisions == newCollisions)
258 return;
259 m_collisions = newCollisions;
260 emit collisionsChanged();
261}
262
263bool QCharacterController::enableShapeHitCallback() const
264{
265 return m_enableShapeHitCallback;
266}
267
268QAbstractPhysXNode *QCharacterController::createPhysXBackend()
269{
270 return new QPhysXCharacterController(this);
271}
272
273void QCharacterController::setEnableShapeHitCallback(bool newEnableShapeHitCallback)
274{
275 if (m_enableShapeHitCallback == newEnableShapeHitCallback)
276 return;
277 m_enableShapeHitCallback = newEnableShapeHitCallback;
278 emit enableShapeHitCallbackChanged();
279}
280
281QT_END_NAMESPACE
282

source code of qtquick3dphysics/src/quick3dphysics/qcharactercontroller.cpp