Line data Source code
1 : /* ragdoll.cpp: ragdoll physics support
2 : *
3 : * Libprimis supports limited client-side ragdolls to simulate corpses. Ragdolls
4 : * are created when actors are killed and support basic physical interaction with
5 : * terrain (but not other players).
6 : *
7 : * This file contains implementation functions for the ragdoll class. For the
8 : * class definition, see ragdoll.h.
9 : */
10 : #include "../libprimis-headers/cube.h"
11 : #include "../../shared/geomexts.h"
12 :
13 : #include <memory>
14 : #include <optional>
15 :
16 : #include "interface/console.h"
17 : #include "interface/control.h"
18 :
19 : #include "render/rendergl.h"
20 :
21 : #include "world/entities.h"
22 : #include "world/octaworld.h"
23 : #include "world/physics.h"
24 : #include "world/bih.h"
25 :
26 : #include "model.h"
27 : #include "ragdoll.h"
28 :
29 : /* ragdollskel */
30 :
31 : //ragdollskel::tri
32 :
33 5 : bool ragdollskel::tri::shareverts(const tri &t) const
34 : {
35 15 : for(int i = 0; i < 3; ++i)
36 : {
37 43 : for(int j = 0; j < 3; ++j)
38 : {
39 33 : if(vert[i] == t.vert[j])
40 : {
41 3 : return true;
42 : }
43 : }
44 : }
45 2 : return false;
46 : }
47 :
48 : //ragdollskel
49 :
50 1 : void ragdollskel::setupjoints()
51 : {
52 4 : for(vert &i : verts)
53 : {
54 3 : i.weight = 0;
55 : }
56 2 : for(joint &j : joints)
57 : {
58 1 : j.weight = 0;
59 1 : vec pos(0, 0, 0);
60 4 : for(int k = 0; k < 3; ++k)
61 : {
62 3 : if(j.vert[k]>=0) //for each valid vertex (not -1) refered to by this joint (up to 3)
63 : {
64 3 : pos.add(verts[j.vert[k]].pos); //j.vert[k].pos is the positon of the vertex pointed to by the joint being used
65 3 : j.weight++;
66 3 : verts[j.vert[k]].weight++;
67 : }
68 : }
69 2 : for(int k = 0; k < j.weight; ++k)
70 : {
71 1 : j.weight = 1/j.weight;
72 : }
73 1 : pos.mul(j.weight);
74 :
75 1 : const tri &t = tris[j.tri];
76 1 : matrix4x3 &m = j.orient;
77 1 : const vec &v1 = verts[t.vert[0]].pos,
78 1 : &v2 = verts[t.vert[1]].pos,
79 1 : &v3 = verts[t.vert[2]].pos;
80 : /*
81 : * /|
82 : * /
83 : * m.c/
84 : * /
85 : * v1 / m.a v2
86 : * *---------->*
87 : * |
88 : * |
89 : * |
90 : * m.b|
91 : * v
92 : *
93 : *
94 : * *
95 : * v3
96 : *
97 : * ----→ ----→
98 : * m.c = v1 v2 x v1 v3
99 : * --→ ----→
100 : * m.b = m.c x v1 v2
101 : *
102 : */
103 1 : m.a = vec(v2).sub(v1).normalize(); //m.a has magnitude 1 at all times
104 1 : m.c.cross(m.a, vec(v3).sub(v1)).normalize(); //m.c will always have a magnitude <=1
105 1 : m.b.cross(m.c, m.a); //m.b will always have a magnitude <= m.c
106 1 : m.d = pos;
107 1 : m.transpose();
108 : }
109 4 : for(vert &i : verts)
110 : {
111 3 : if(i.weight)
112 : {
113 3 : i.weight = 1/i.weight;
114 : }
115 : }
116 1 : reljoints.clear();
117 1 : }
118 :
119 1 : void ragdollskel::setuprotfrictions()
120 : {
121 1 : rotfrictions.clear();
122 3 : for(size_t i = 0; i < tris.size(); i++)
123 : {
124 3 : for(size_t j = i+1; j < tris.size(); j++)
125 : {
126 1 : if(tris[i].shareverts(tris[j]))
127 : {
128 : rotfriction r;
129 1 : r.tri[0] = i;
130 1 : r.tri[1] = j;
131 1 : rotfrictions.push_back(r);
132 : }
133 : }
134 : }
135 1 : }
136 :
137 1 : void ragdollskel::setup()
138 : {
139 1 : setupjoints();
140 1 : setuprotfrictions();
141 :
142 1 : loaded = true;
143 1 : }
144 :
145 1 : void ragdollskel::addreljoint(int bone, int parent)
146 : {
147 1 : reljoints.emplace_back(reljoint{bone, parent});
148 1 : }
149 : /* ragdolldata */
150 :
151 3 : ragdolldata::ragdolldata(const ragdollskel *skel, float scale)
152 3 : : skel(skel),
153 3 : millis(lastmillis),
154 3 : collidemillis(0),
155 3 : lastmove(lastmillis),
156 3 : radius(0),
157 6 : tris(skel->tris.size()),
158 3 : animjoints(!skel->animjoints || skel->joints.empty() ? nullptr : new matrix4x3[skel->joints.size()]),
159 3 : reljoints(skel->reljoints.empty() ? nullptr : new dualquat[skel->reljoints.size()]),
160 6 : verts(skel->verts.size()),
161 3 : collisions(0),
162 3 : floating(0),
163 3 : unsticks(INT_MAX),
164 3 : timestep(0),
165 3 : scale(scale),
166 9 : rotfrictions(skel->rotfrictions.size())
167 : {
168 3 : }
169 :
170 3 : ragdolldata::~ragdolldata()
171 : {
172 3 : if(animjoints)
173 : {
174 1 : delete[] animjoints;
175 : }
176 3 : if(reljoints)
177 : {
178 0 : delete[] reljoints;
179 : }
180 3 : }
181 :
182 : /*
183 : seed particle position = avg(modelview * base2anim * spherepos)
184 : mapped transform = invert(curtri) * origtrig
185 : parented transform = parent{invert(curtri) * origtrig} * (invert(parent{base2anim}) * base2anim)
186 : */
187 :
188 1 : matrix4x3 ragdolldata::calcanimjoint(int i, const matrix4x3 &anim) const
189 : {
190 1 : const ragdollskel::joint &j = skel->joints[i];
191 1 : vec pos(0, 0, 0);
192 4 : for(int k = 0; k < 3; ++k)
193 : {
194 3 : if(j.vert[k]>=0)
195 : {
196 3 : pos.add(verts[j.vert[k]].pos);
197 : }
198 : }
199 1 : pos.mul(j.weight);
200 :
201 1 : const ragdollskel::tri &t = skel->tris[j.tri];
202 1 : matrix4x3 m;
203 1 : const vec &v1 = verts[t.vert[0]].pos,
204 1 : &v2 = verts[t.vert[1]].pos,
205 1 : &v3 = verts[t.vert[2]].pos;
206 1 : m.a = vec(v2).sub(v1).normalize();
207 1 : m.c.cross(m.a, vec(v3).sub(v1)).normalize();
208 1 : m.b.cross(m.c, m.a);
209 1 : m.d = pos;
210 1 : matrix4x3 result;
211 1 : result.transposemul(m, anim);
212 2 : return result;
213 : }
214 :
215 1 : void ragdolldata::calctris()
216 : {
217 1 : for(size_t i = 0; i < skel->tris.size(); i++)
218 : {
219 0 : const ragdollskel::tri &t = skel->tris[i];
220 0 : matrix3 &m = tris[i];
221 0 : const vec &v1 = verts[t.vert[0]].pos,
222 0 : &v2 = verts[t.vert[1]].pos,
223 0 : &v3 = verts[t.vert[2]].pos;
224 0 : m.a = vec(v2).sub(v1).normalize();
225 0 : m.c.cross(m.a, vec(v3).sub(v1)).normalize();
226 0 : m.b.cross(m.c, m.a);
227 : }
228 1 : }
229 :
230 1 : void ragdolldata::calcboundsphere()
231 : {
232 1 : center = vec(0, 0, 0);
233 3 : for(const vert &v : verts)
234 : {
235 2 : center.add(v.pos);
236 : }
237 1 : center.div(verts.size());
238 1 : radius = 0;
239 3 : for(const vert &v : verts)
240 : {
241 2 : radius = std::max(radius, v.pos.dist(center));
242 : }
243 1 : }
244 :
245 : VAR(ragdolltimestepmin, 1, 5, 50);
246 : VAR(ragdolltimestepmax, 1, 10, 50);
247 :
248 1 : void ragdolldata::init(const dynent *d)
249 : {
250 1 : float ts = ragdolltimestepmin/1000.0f;
251 3 : for(vert &v : verts)
252 : {
253 2 : (v.oldpos = v.pos).sub(vec(d->vel).add(d->falling).mul(ts));
254 : }
255 1 : timestep = ts;
256 :
257 1 : calctris();
258 1 : calcboundsphere();
259 1 : offset = d->o;
260 1 : offset.sub(skel->eye >= 0 ? verts[skel->eye].pos : center);
261 1 : offset.z += (d->eyeheight + d->aboveeye)/2;
262 1 : }
263 :
264 0 : void ragdolldata::constraindist()
265 : {
266 0 : for(const ragdollskel::distlimit &d : skel->distlimits)
267 : {
268 0 : vert &v1 = verts[d.vert[0]],
269 0 : &v2 = verts[d.vert[1]];
270 0 : vec dir = vec(v2.pos).sub(v1.pos);
271 0 : const float dist = dir.magnitude()/scale;
272 : float cdist;
273 0 : if(dist < d.mindist)
274 : {
275 0 : cdist = d.mindist;
276 : }
277 0 : else if(dist > d.maxdist)
278 : {
279 0 : cdist = d.maxdist;
280 : }
281 : else
282 : {
283 0 : continue;
284 : }
285 0 : if(dist > 1e-4f)
286 : {
287 0 : dir.mul(cdist*0.5f/dist);
288 : }
289 : else
290 : {
291 0 : dir = vec(0, 0, cdist*0.5f*scale);
292 : }
293 0 : const vec center = vec(v1.pos).add(v2.pos).mul(0.5f);
294 0 : v1.newpos.add(vec(center).sub(dir));
295 0 : v1.weight++;
296 0 : v2.newpos.add(vec(center).add(dir));
297 0 : v2.weight++;
298 : }
299 0 : }
300 :
301 0 : void ragdolldata::applyrotlimit(const ragdollskel::tri &t1, const ragdollskel::tri &t2, float angle, const vec &axis)
302 : {
303 0 : vert &v1a = verts[t1.vert[0]],
304 0 : &v1b = verts[t1.vert[1]],
305 0 : &v1c = verts[t1.vert[2]],
306 0 : &v2a = verts[t2.vert[0]],
307 0 : &v2b = verts[t2.vert[1]],
308 0 : &v2c = verts[t2.vert[2]];
309 : // vec() copy constructor used below to deal with the fact that vec.add/sub() are destructive operations
310 0 : const vec m1 = vec(v1a.pos).add(v1b.pos).add(v1c.pos).div(3),
311 0 : m2 = vec(v2a.pos).add(v2b.pos).add(v2c.pos).div(3);
312 0 : vec q1a, q1b, q1c, q2a, q2b, q2c;
313 0 : const float w1 = q1a.cross(axis, vec(v1a.pos).sub(m1)).magnitude() +
314 0 : q1b.cross(axis, vec(v1b.pos).sub(m1)).magnitude() +
315 0 : q1c.cross(axis, vec(v1c.pos).sub(m1)).magnitude(),
316 0 : w2 = q2a.cross(axis, vec(v2a.pos).sub(m2)).magnitude() +
317 0 : q2b.cross(axis, vec(v2b.pos).sub(m2)).magnitude() +
318 0 : q2c.cross(axis, vec(v2c.pos).sub(m2)).magnitude();
319 0 : angle /= w1 + w2 + 1e-9f;
320 0 : const float a1 = angle*w2,
321 0 : a2 = -angle*w1,
322 0 : s1 = std::sin(a1),
323 0 : s2 = std::sin(a2);
324 0 : const vec c1 = vec(axis).mul(1 - std::cos(a1)),
325 0 : c2 = vec(axis).mul(1 - std::cos(a2));
326 0 : v1a.newpos.add(vec().cross(c1, q1a).madd(q1a, s1).add(v1a.pos));
327 0 : v1a.weight++;
328 0 : v1b.newpos.add(vec().cross(c1, q1b).madd(q1b, s1).add(v1b.pos));
329 0 : v1b.weight++;
330 0 : v1c.newpos.add(vec().cross(c1, q1c).madd(q1c, s1).add(v1c.pos));
331 0 : v1c.weight++;
332 0 : v2a.newpos.add(vec().cross(c2, q2a).madd(q2a, s2).add(v2a.pos));
333 0 : v2a.weight++;
334 0 : v2b.newpos.add(vec().cross(c2, q2b).madd(q2b, s2).add(v2b.pos));
335 0 : v2b.weight++;
336 0 : v2c.newpos.add(vec().cross(c2, q2c).madd(q2c, s2).add(v2c.pos));
337 0 : v2c.weight++;
338 0 : }
339 :
340 0 : void ragdolldata::constrainrot()
341 : {
342 0 : calctris(); //set up shadowing matrix3 objects for skel->tris
343 0 : for(const ragdollskel::rotlimit &r : skel->rotlimits)
344 : {
345 0 : matrix3 rot;
346 0 : rot.mul(tris[r.tri[0]], r.middle);
347 0 : rot.multranspose(tris[r.tri[1]]);
348 :
349 0 : vec axis;
350 : float angle;
351 0 : const float tr = rot.trace();
352 : //calculate angle and axis from the matrix `rot`, if possible
353 0 : if(tr >= r.maxtrace || !rot.calcangleaxis(tr, angle, axis))
354 : {
355 0 : continue;
356 : }
357 0 : angle = r.maxangle - angle + 1e-3f;
358 0 : applyrotlimit(skel->tris[r.tri[0]], skel->tris[r.tri[1]], angle, axis);
359 : }
360 0 : }
361 :
362 0 : void ragdolldata::calcrotfriction()
363 : {
364 0 : for(size_t i = 0; i < skel->rotfrictions.size(); ++i)
365 : {
366 0 : rotfrictions[i].transposemul(tris[skel->rotfrictions[i].tri[0]], tris[skel->rotfrictions[i].tri[1]]);
367 : }
368 0 : }
369 :
370 0 : void ragdolldata::applyrotfriction(float ts)
371 : {
372 0 : static FVAR(ragdollrotfric, 0, 0.85f, 1);
373 0 : static FVAR(ragdollrotfricstop, 0, 0.1f, 1);
374 :
375 0 : calctris();
376 0 : float stopangle = 2*M_PI*ts*ragdollrotfricstop,
377 0 : rotfric = 1.0f - std::pow(ragdollrotfric, ts*1000.0f/ragdolltimestepmin);
378 0 : for(size_t i = 0; i < skel->rotfrictions.size(); ++i)
379 : {
380 0 : matrix3 rot;
381 0 : rot.mul(tris[ skel->rotfrictions[i].tri[0]], rotfrictions[i]);
382 0 : rot.multranspose(tris[ skel->rotfrictions[i].tri[1]]);
383 :
384 0 : vec axis;
385 : float angle;
386 0 : if(!rot.calcangleaxis(angle, axis))
387 : {
388 0 : continue;
389 : }
390 0 : angle *= -(std::fabs(angle) >= stopangle ? rotfric : 1.0f);
391 0 : applyrotlimit(skel->tris[skel->rotfrictions[i].tri[0]], skel->tris[ skel->rotfrictions[i].tri[1]], angle, axis);
392 : }
393 0 : for(vert &v : verts)
394 : {
395 0 : if(!v.weight)
396 : {
397 0 : continue;
398 : }
399 0 : v.pos = v.newpos.div(v.weight);
400 0 : v.newpos = vec(0, 0, 0);
401 0 : v.weight = 0;
402 : }
403 0 : }
404 :
405 0 : void ragdolldata::tryunstick(float speed, vec &cwall)
406 : {
407 : /* vec `unstuck` is the average position of all vertices with the `stuck` flag
408 : * that are not found to collide with the world, as well as all vertices without
409 : * the `stuck` flag.
410 : */
411 0 : vec unstuck(0, 0, 0);
412 0 : size_t stuck = 0;
413 0 : for(size_t i = 0; i < verts.size(); i++)
414 : {
415 0 : vert &v = verts[i];
416 0 : if(v.stuck)
417 : {
418 0 : if(collidevert(v.pos, vec(0, 0, 0), skel->verts[i].radius, cwall))
419 : {
420 0 : stuck++;
421 0 : continue;
422 : }
423 0 : v.stuck = false;
424 : }
425 0 : unstuck.add(v.pos);
426 : }
427 0 : unsticks = 0;
428 0 : if(!stuck || stuck >= verts.size())
429 : {
430 0 : return;
431 : }
432 0 : unstuck.div(verts.size() - stuck); //get average by dividing by # verts not stuck
433 :
434 0 : for(vert &v : verts)
435 : {
436 0 : if(v.stuck)
437 : {
438 0 : v.pos.add(vec(unstuck).sub(v.pos).rescale(speed));
439 0 : unsticks++;
440 : }
441 : }
442 : }
443 :
444 0 : void ragdolldata::constrain(vec &cwall)
445 : {
446 0 : static VAR(ragdollconstrain, 1, 7, 100); //number of iterations to run ragdolldata::constrain() for
447 : //note: this for loop does not use the loop variable `i` anywhere
448 0 : for(int i = 0; i < ragdollconstrain; ++i)
449 : {
450 0 : constraindist();
451 0 : for(vert &v : verts)
452 : {
453 0 : v.undo = v.pos;
454 0 : if(v.weight)
455 : {
456 0 : v.pos = v.newpos.div(v.weight);
457 0 : v.newpos = vec(0, 0, 0);
458 0 : v.weight = 0;
459 : }
460 : }
461 :
462 0 : constrainrot();
463 0 : for(size_t j = 0; j < verts.size(); j++)
464 : {
465 0 : vert &v = verts[j];
466 0 : if(v.weight)
467 : {
468 0 : v.pos = v.newpos.div(v.weight);
469 0 : v.newpos = vec(0, 0, 0);
470 0 : v.weight = 0;
471 : }
472 0 : if(v.pos != v.undo && collidevert(v.pos, vec(v.pos).sub(v.undo), skel->verts[j].radius, cwall))
473 : {
474 0 : vec dir = vec(v.pos).sub(v.oldpos);
475 0 : const float facing = dir.dot(cwall);
476 0 : if(facing < 0)
477 : {
478 0 : v.oldpos = vec(v.undo).sub(dir.msub(cwall, 2*facing));
479 : }
480 0 : v.pos = v.undo;
481 0 : v.collided = true;
482 : }
483 : }
484 : }
485 0 : }
486 :
487 0 : void ragdolldata::move(bool water, float ts)
488 : {
489 :
490 0 : static FVAR(ragdollbodyfric, 0, 0.95f, 1);
491 0 : static FVAR(ragdollbodyfricscale, 0, 2, 10);
492 0 : static FVAR(ragdollwaterfric, 0, 0.85f, 1);
493 0 : static FVAR(ragdollgroundfric, 0, 0.8f, 1);
494 0 : static FVAR(ragdollairfric, 0, 0.996f, 1);
495 0 : static FVAR(ragdollunstick, 0, 10, 1e3f);
496 0 : static VAR(ragdollexpireoffset, 0, 2500, 30000);
497 0 : static VAR(ragdollwaterexpireoffset, 0, 4000, 30000);
498 :
499 0 : if(collidemillis && lastmillis > collidemillis)
500 : {
501 0 : return;
502 : }
503 :
504 0 : calcrotfriction();
505 0 : const float tsfric = timestep ? ts/timestep : 1,
506 0 : airfric = ragdollairfric + std::min((ragdollbodyfricscale*collisions)/verts.size(), 1.0f)*(ragdollbodyfric - ragdollairfric);
507 0 : collisions = 0;
508 0 : vec collidewall(0,0,0);
509 :
510 0 : for(size_t i = 0; i < verts.size(); i++)
511 : {
512 0 : vert &v = verts[i];
513 0 : vec dpos = vec(v.pos).sub(v.oldpos);
514 0 : dpos.z -= 100*ts*ts;
515 0 : if(water)
516 : {
517 : //reinterpret cast pointer -> "random" seed value
518 0 : dpos.z += 0.25f*std::sin(detrnd(reinterpret_cast<size_t>(this)+i, 360)/RAD + lastmillis/10000.0f*M_PI)*ts;
519 : }
520 0 : dpos.mul(std::pow((water ? ragdollwaterfric : 1.0f) * (v.collided ? ragdollgroundfric : airfric), ts*1000.0f/ragdolltimestepmin)*tsfric);
521 0 : v.oldpos = v.pos;
522 0 : v.pos.add(dpos);
523 : }
524 0 : applyrotfriction(ts);
525 0 : for(size_t i = 0; i < verts.size(); i++)
526 : {
527 0 : vert &v = verts[i];
528 0 : if(v.pos.z < 0)
529 : {
530 0 : v.pos.z = 0;
531 0 : v.oldpos = v.pos;
532 0 : collisions++;
533 : }
534 0 : vec dir = vec(v.pos).sub(v.oldpos);
535 0 : v.collided = collidevert(v.pos, dir, skel->verts[i].radius, collidewall);
536 0 : if(v.collided)
537 : {
538 0 : v.pos = v.oldpos;
539 0 : v.oldpos.sub(dir.reflect(collidewall));
540 0 : collisions++;
541 : }
542 : }
543 0 : if(unsticks && ragdollunstick)
544 : {
545 0 : tryunstick(ts*ragdollunstick, collidewall);
546 : }
547 0 : timestep = ts;
548 0 : if(collisions)
549 : {
550 0 : floating = 0;
551 0 : if(!collidemillis)
552 : {
553 0 : collidemillis = lastmillis + (water ? ragdollwaterexpireoffset : ragdollexpireoffset);
554 : }
555 : }
556 0 : else if(++floating > 1 && lastmillis < collidemillis)
557 : {
558 0 : collidemillis = 0;
559 : }
560 0 : constrain(collidewall);
561 0 : calctris();
562 0 : calcboundsphere();
563 : }
564 :
565 0 : bool ragdolldata::collidevert(const vec &pos, const vec &dir, float radius, vec &cwall)
566 : {
567 : static struct vertent : physent
568 : {
569 0 : vertent()
570 0 : {
571 0 : type = PhysEnt_Bounce;
572 0 : radius = xradius = yradius = eyeheight = aboveeye = 1;
573 0 : }
574 0 : } v;
575 0 : v.o = pos;
576 0 : if(v.radius != radius) //(radius param != 1)
577 : {
578 0 : v.radius = v.xradius = v.yradius = v.eyeheight = v.aboveeye = radius;
579 : }
580 : //collide generated vertent (point with sphere bounding of r = radius)
581 : //with dir parameter
582 0 : return collide(&v, &cwall, dir, 0);
583 : }
584 :
585 : //used in iengine
586 0 : void moveragdoll(dynent *d)
587 : {
588 0 : static FVAR(ragdolleyesmooth, 0, 0.5f, 1);
589 0 : static VAR(ragdolleyesmoothmillis, 1, 250, 10000);
590 0 : if(!curtime || !d->ragdoll)
591 : {
592 0 : return;
593 : }
594 0 : if(!d->ragdoll->collidemillis || lastmillis < d->ragdoll->collidemillis)
595 : {
596 0 : const int lastmove = d->ragdoll->lastmove;
597 0 : while(d->ragdoll->lastmove + (lastmove == d->ragdoll->lastmove ? ragdolltimestepmin : ragdolltimestepmax) <= lastmillis)
598 : {
599 0 : const int timestep = std::min(ragdolltimestepmax, lastmillis - d->ragdoll->lastmove),
600 0 : material = rootworld.lookupmaterial(vec(d->ragdoll->center.x, d->ragdoll->center.y, d->ragdoll->center.z + d->ragdoll->radius/2));
601 0 : const bool water = (material&MatFlag_Volume) == Mat_Water;
602 0 : d->inwater = water ? material&MatFlag_Volume : Mat_Air;
603 0 : d->ragdoll->move(water, timestep/1000.0f);
604 0 : d->ragdoll->lastmove += timestep;
605 : }
606 : }
607 :
608 0 : vec eye = d->ragdoll->skel->eye >= 0 ? d->ragdoll->verts[d->ragdoll->skel->eye].pos : d->ragdoll->center;
609 0 : eye.add(d->ragdoll->offset);
610 0 : const float k = std::pow(ragdolleyesmooth, static_cast<float>(curtime)/ragdolleyesmoothmillis);
611 0 : d->o.lerp(eye, 1-k);
612 : }
613 :
614 1 : void cleanragdoll(dynent *d)
615 : {
616 1 : if(d->ragdoll)
617 : {
618 1 : delete d->ragdoll;
619 1 : d->ragdoll = nullptr;
620 : }
621 1 : }
|