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

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