diff options
author | Robert Adams | 2012-07-06 10:01:47 -0700 |
---|---|---|
committer | Robert Adams | 2012-07-06 15:09:19 -0700 |
commit | e4a6611865848ffcfa6adedd813534e0a0e4abf3 (patch) | |
tree | c01ee1f828f276d45b09fbebea08b4e730e721ef | |
parent | Add assert to attachment regression tests to check that number of objects in ... (diff) | |
download | opensim-SC_OLD-e4a6611865848ffcfa6adedd813534e0a0e4abf3.zip opensim-SC_OLD-e4a6611865848ffcfa6adedd813534e0a0e4abf3.tar.gz opensim-SC_OLD-e4a6611865848ffcfa6adedd813534e0a0e4abf3.tar.bz2 opensim-SC_OLD-e4a6611865848ffcfa6adedd813534e0a0e4abf3.tar.xz |
Clean up collision reporting code so they are properly passed to
the simulator in batches.
More comments.
-rw-r--r-- | OpenSim/Region/Physics/BulletSPlugin/BSCharacter.cs | 102 | ||||
-rw-r--r-- | OpenSim/Region/Physics/BulletSPlugin/BSPlugin.cs | 11 | ||||
-rw-r--r-- | OpenSim/Region/Physics/BulletSPlugin/BSPrim.cs | 29 | ||||
-rw-r--r-- | OpenSim/Region/Physics/BulletSPlugin/BSScene.cs | 23 |
4 files changed, 107 insertions, 58 deletions
diff --git a/OpenSim/Region/Physics/BulletSPlugin/BSCharacter.cs b/OpenSim/Region/Physics/BulletSPlugin/BSCharacter.cs index b08d5db..dc0c008 100644 --- a/OpenSim/Region/Physics/BulletSPlugin/BSCharacter.cs +++ b/OpenSim/Region/Physics/BulletSPlugin/BSCharacter.cs | |||
@@ -74,7 +74,7 @@ public class BSCharacter : PhysicsActor | |||
74 | private float _buoyancy; | 74 | private float _buoyancy; |
75 | 75 | ||
76 | private int _subscribedEventsMs = 0; | 76 | private int _subscribedEventsMs = 0; |
77 | private int _lastCollisionTime = 0; | 77 | private int _nextCollisionOkTime = 0; |
78 | 78 | ||
79 | private Vector3 _PIDTarget; | 79 | private Vector3 _PIDTarget; |
80 | private bool _usePID; | 80 | private bool _usePID; |
@@ -360,17 +360,22 @@ public class BSCharacter : PhysicsActor | |||
360 | } | 360 | } |
361 | //m_lastUpdateSent = false; | 361 | //m_lastUpdateSent = false; |
362 | } | 362 | } |
363 | |||
363 | public override void AddAngularForce(Vector3 force, bool pushforce) { | 364 | public override void AddAngularForce(Vector3 force, bool pushforce) { |
364 | } | 365 | } |
365 | public override void SetMomentum(Vector3 momentum) { | 366 | public override void SetMomentum(Vector3 momentum) { |
366 | } | 367 | } |
368 | |||
369 | // Turn on collision events at a rate no faster than one every the given milliseconds | ||
367 | public override void SubscribeEvents(int ms) { | 370 | public override void SubscribeEvents(int ms) { |
368 | _subscribedEventsMs = ms; | 371 | _subscribedEventsMs = ms; |
369 | _lastCollisionTime = Util.EnvironmentTickCount() - _subscribedEventsMs; // make first collision happen | 372 | _nextCollisionOkTime = Util.EnvironmentTickCount() - _subscribedEventsMs; // make first collision happen |
370 | } | 373 | } |
374 | // Stop collision events | ||
371 | public override void UnSubscribeEvents() { | 375 | public override void UnSubscribeEvents() { |
372 | _subscribedEventsMs = 0; | 376 | _subscribedEventsMs = 0; |
373 | } | 377 | } |
378 | // Return 'true' if someone has subscribed to events | ||
374 | public override bool SubscribedEvents() { | 379 | public override bool SubscribedEvents() { |
375 | return (_subscribedEventsMs > 0); | 380 | return (_subscribedEventsMs > 0); |
376 | } | 381 | } |
@@ -386,47 +391,57 @@ public class BSCharacter : PhysicsActor | |||
386 | _mass = _density * _avatarVolume; | 391 | _mass = _density * _avatarVolume; |
387 | } | 392 | } |
388 | 393 | ||
394 | // Set to 'true' if the individual changed items should be checked | ||
395 | // (someday RequestPhysicsTerseUpdate() will take a bitmap of changed properties) | ||
396 | const bool SHOULD_CHECK_FOR_INDIVIDUAL_CHANGES = false; | ||
397 | |||
389 | // The physics engine says that properties have updated. Update same and inform | 398 | // The physics engine says that properties have updated. Update same and inform |
390 | // the world that things have changed. | 399 | // the world that things have changed. |
391 | public void UpdateProperties(EntityProperties entprop) | 400 | public void UpdateProperties(EntityProperties entprop) |
392 | { | 401 | { |
393 | bool changed = false; | 402 | bool changed = false; |
394 | // we assign to the local variables so the normal set action does not happen | 403 | if (SHOULD_CHECK_FOR_INDIVIDUAL_CHANGES) { |
395 | if (_position != entprop.Position) | 404 | // we assign to the local variables so the normal set action does not happen |
396 | { | 405 | if (_position != entprop.Position) { |
397 | _position = entprop.Position; | 406 | _position = entprop.Position; |
398 | changed = true; | 407 | changed = true; |
408 | } | ||
409 | if (_orientation != entprop.Rotation) { | ||
410 | _orientation = entprop.Rotation; | ||
411 | changed = true; | ||
412 | } | ||
413 | if (_velocity != entprop.Velocity) { | ||
414 | _velocity = entprop.Velocity; | ||
415 | changed = true; | ||
416 | } | ||
417 | if (_acceleration != entprop.Acceleration) { | ||
418 | _acceleration = entprop.Acceleration; | ||
419 | changed = true; | ||
420 | } | ||
421 | if (_rotationalVelocity != entprop.RotationalVelocity) { | ||
422 | _rotationalVelocity = entprop.RotationalVelocity; | ||
423 | changed = true; | ||
424 | } | ||
425 | if (changed) { | ||
426 | // m_log.DebugFormat("{0}: UpdateProperties: id={1}, c={2}, pos={3}, rot={4}", LogHeader, LocalID, changed, _position, _orientation); | ||
427 | // Avatar movement is not done by generating this event. There is code in the heartbeat | ||
428 | // loop that updates avatars. | ||
429 | // base.RequestPhysicsterseUpdate(); | ||
430 | } | ||
399 | } | 431 | } |
400 | if (_orientation != entprop.Rotation) | 432 | else { |
401 | { | 433 | _position = entprop.Position; |
402 | _orientation = entprop.Rotation; | 434 | _orientation = entprop.Rotation; |
403 | changed = true; | ||
404 | } | ||
405 | if (_velocity != entprop.Velocity) | ||
406 | { | ||
407 | _velocity = entprop.Velocity; | 435 | _velocity = entprop.Velocity; |
408 | changed = true; | ||
409 | } | ||
410 | if (_acceleration != entprop.Acceleration) | ||
411 | { | ||
412 | _acceleration = entprop.Acceleration; | 436 | _acceleration = entprop.Acceleration; |
413 | changed = true; | ||
414 | } | ||
415 | if (_rotationalVelocity != entprop.RotationalVelocity) | ||
416 | { | ||
417 | _rotationalVelocity = entprop.RotationalVelocity; | 437 | _rotationalVelocity = entprop.RotationalVelocity; |
418 | changed = true; | ||
419 | } | ||
420 | if (changed) | ||
421 | { | ||
422 | // m_log.DebugFormat("{0}: UpdateProperties: id={1}, c={2}, pos={3}, rot={4}", LogHeader, LocalID, changed, _position, _orientation); | ||
423 | // Avatar movement is not done by generating this event. There is a system that | ||
424 | // checks for avatar updates each heartbeat loop. | ||
425 | // base.RequestPhysicsterseUpdate(); | 438 | // base.RequestPhysicsterseUpdate(); |
426 | } | 439 | } |
427 | } | 440 | } |
428 | 441 | ||
429 | // Called by the scene when a collision with this object is reported | 442 | // Called by the scene when a collision with this object is reported |
443 | // The collision, if it should be reported to the character, is placed in a collection | ||
444 | // that will later be sent to the simulator when SendCollisions() is called. | ||
430 | CollisionEventUpdate collisionCollection = null; | 445 | CollisionEventUpdate collisionCollection = null; |
431 | public void Collide(uint collidingWith, ActorTypes type, Vector3 contactPoint, Vector3 contactNormal, float pentrationDepth) | 446 | public void Collide(uint collidingWith, ActorTypes type, Vector3 contactPoint, Vector3 contactNormal, float pentrationDepth) |
432 | { | 447 | { |
@@ -440,29 +455,34 @@ public class BSCharacter : PhysicsActor | |||
440 | } | 455 | } |
441 | 456 | ||
442 | // throttle collisions to the rate specified in the subscription | 457 | // throttle collisions to the rate specified in the subscription |
443 | if (_subscribedEventsMs == 0) return; // don't want collisions | 458 | if (_subscribedEventsMs != 0) { |
444 | int nowTime = _scene.SimulationNowTime; | 459 | int nowTime = _scene.SimulationNowTime; |
445 | if (nowTime < (_lastCollisionTime + _subscribedEventsMs)) return; | 460 | if (nowTime >= _nextCollisionOkTime) { |
446 | _lastCollisionTime = nowTime; | 461 | _nextCollisionOkTime = nowTime + _subscribedEventsMs; |
447 | 462 | ||
448 | if (collisionCollection == null) | 463 | if (collisionCollection == null) |
449 | collisionCollection = new CollisionEventUpdate(); | 464 | collisionCollection = new CollisionEventUpdate(); |
450 | collisionCollection.AddCollider(collidingWith, new ContactPoint(contactPoint, contactNormal, pentrationDepth)); | 465 | collisionCollection.AddCollider(collidingWith, new ContactPoint(contactPoint, contactNormal, pentrationDepth)); |
466 | } | ||
467 | } | ||
451 | } | 468 | } |
452 | 469 | ||
453 | public void SendCollisions() | 470 | public void SendCollisions() |
454 | { | 471 | { |
455 | // if (collisionCollection != null) | 472 | /* |
456 | // { | 473 | if (collisionCollection != null && collisionCollection.Count > 0) |
457 | // base.SendCollisionUpdate(collisionCollection); | 474 | { |
458 | // collisionCollection = null; | 475 | base.SendCollisionUpdate(collisionCollection); |
459 | // } | 476 | collisionCollection = null; |
477 | } | ||
478 | */ | ||
460 | // Kludge to make a collision call even if there are no collisions. | 479 | // Kludge to make a collision call even if there are no collisions. |
461 | // This causes the avatar animation to get updated. | 480 | // This causes the avatar animation to get updated. |
462 | if (collisionCollection == null) | 481 | if (collisionCollection == null) |
463 | collisionCollection = new CollisionEventUpdate(); | 482 | collisionCollection = new CollisionEventUpdate(); |
464 | base.SendCollisionUpdate(collisionCollection); | 483 | base.SendCollisionUpdate(collisionCollection); |
465 | collisionCollection = null; | 484 | collisionCollection.Clear(); |
485 | // End kludge | ||
466 | } | 486 | } |
467 | 487 | ||
468 | } | 488 | } |
diff --git a/OpenSim/Region/Physics/BulletSPlugin/BSPlugin.cs b/OpenSim/Region/Physics/BulletSPlugin/BSPlugin.cs index 0730824..0f027b8 100644 --- a/OpenSim/Region/Physics/BulletSPlugin/BSPlugin.cs +++ b/OpenSim/Region/Physics/BulletSPlugin/BSPlugin.cs | |||
@@ -32,6 +32,14 @@ using OpenMetaverse; | |||
32 | 32 | ||
33 | namespace OpenSim.Region.Physics.BulletSPlugin | 33 | namespace OpenSim.Region.Physics.BulletSPlugin |
34 | { | 34 | { |
35 | /// <summary> | ||
36 | /// Entry for a port of Bullet (http://bulletphysics.org/) to OpenSim. | ||
37 | /// This module interfaces to an unmanaged C++ library which makes the | ||
38 | /// actual calls into the Bullet physics engine. | ||
39 | /// The unmanaged library is found in opensim-libs::trunk/unmanaged/BulletSim/. | ||
40 | /// The unmanaged library is compiled and linked statically with Bullet | ||
41 | /// to create BulletSim.dll and libBulletSim.so (for both 32 and 64 bit). | ||
42 | /// </summary> | ||
35 | public class BSPlugin : IPhysicsPlugin | 43 | public class BSPlugin : IPhysicsPlugin |
36 | { | 44 | { |
37 | //private static readonly log4net.ILog m_log = log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType); | 45 | //private static readonly log4net.ILog m_log = log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType); |
@@ -53,6 +61,9 @@ public class BSPlugin : IPhysicsPlugin | |||
53 | { | 61 | { |
54 | if (Util.IsWindows()) | 62 | if (Util.IsWindows()) |
55 | Util.LoadArchSpecificWindowsDll("BulletSim.dll"); | 63 | Util.LoadArchSpecificWindowsDll("BulletSim.dll"); |
64 | // If not Windows, loading is performed by the | ||
65 | // Mono loader as specified in | ||
66 | // "bin/Physics/OpenSim.Region.Physics.BulletSPlugin.dll.config". | ||
56 | 67 | ||
57 | _mScene = new BSScene(sceneIdentifier); | 68 | _mScene = new BSScene(sceneIdentifier); |
58 | } | 69 | } |
diff --git a/OpenSim/Region/Physics/BulletSPlugin/BSPrim.cs b/OpenSim/Region/Physics/BulletSPlugin/BSPrim.cs index 248d1f2..130f1ca 100644 --- a/OpenSim/Region/Physics/BulletSPlugin/BSPrim.cs +++ b/OpenSim/Region/Physics/BulletSPlugin/BSPrim.cs | |||
@@ -90,7 +90,7 @@ public sealed class BSPrim : PhysicsActor | |||
90 | private BSPrim _parentPrim; | 90 | private BSPrim _parentPrim; |
91 | 91 | ||
92 | private int _subscribedEventsMs = 0; | 92 | private int _subscribedEventsMs = 0; |
93 | private int _lastCollisionTime = 0; | 93 | private int _nextCollisionOkTime = 0; |
94 | long _collidingStep; | 94 | long _collidingStep; |
95 | long _collidingGroundStep; | 95 | long _collidingGroundStep; |
96 | 96 | ||
@@ -597,7 +597,8 @@ public sealed class BSPrim : PhysicsActor | |||
597 | } | 597 | } |
598 | public override void SubscribeEvents(int ms) { | 598 | public override void SubscribeEvents(int ms) { |
599 | _subscribedEventsMs = ms; | 599 | _subscribedEventsMs = ms; |
600 | _lastCollisionTime = Util.EnvironmentTickCount() - _subscribedEventsMs; // make first collision happen | 600 | // make sure first collision happens |
601 | _nextCollisionOkTime = Util.EnvironmentTickCount() - _subscribedEventsMs; | ||
601 | } | 602 | } |
602 | public override void UnSubscribeEvents() { | 603 | public override void UnSubscribeEvents() { |
603 | _subscribedEventsMs = 0; | 604 | _subscribedEventsMs = 0; |
@@ -1338,23 +1339,27 @@ public sealed class BSPrim : PhysicsActor | |||
1338 | _collidingGroundStep = _scene.SimulationStep; | 1339 | _collidingGroundStep = _scene.SimulationStep; |
1339 | } | 1340 | } |
1340 | 1341 | ||
1341 | if (_subscribedEventsMs == 0) return; // nothing in the object is waiting for collision events | 1342 | // if someone is subscribed to collision events.... |
1342 | // throttle the collisions to the number of milliseconds specified in the subscription | 1343 | if (_subscribedEventsMs != 0) { |
1343 | int nowTime = _scene.SimulationNowTime; | 1344 | // throttle the collisions to the number of milliseconds specified in the subscription |
1344 | if (nowTime < (_lastCollisionTime + _subscribedEventsMs)) return; | 1345 | int nowTime = _scene.SimulationNowTime; |
1345 | _lastCollisionTime = nowTime; | 1346 | if (nowTime >= _nextCollisionOkTime) { |
1347 | _nextCollisionOkTime = nowTime + _subscribedEventsMs; | ||
1346 | 1348 | ||
1347 | if (collisionCollection == null) | 1349 | if (collisionCollection == null) |
1348 | collisionCollection = new CollisionEventUpdate(); | 1350 | collisionCollection = new CollisionEventUpdate(); |
1349 | collisionCollection.AddCollider(collidingWith, new ContactPoint(contactPoint, contactNormal, pentrationDepth)); | 1351 | collisionCollection.AddCollider(collidingWith, new ContactPoint(contactPoint, contactNormal, pentrationDepth)); |
1352 | } | ||
1353 | } | ||
1350 | } | 1354 | } |
1351 | 1355 | ||
1356 | // The scene is telling us it's time to pass our collected collisions into the simulator | ||
1352 | public void SendCollisions() | 1357 | public void SendCollisions() |
1353 | { | 1358 | { |
1354 | if (collisionCollection != null) | 1359 | if (collisionCollection != null && collisionCollection.Count > 0) |
1355 | { | 1360 | { |
1356 | base.SendCollisionUpdate(collisionCollection); | 1361 | base.SendCollisionUpdate(collisionCollection); |
1357 | collisionCollection = null; | 1362 | collisionCollection.Clear(); |
1358 | } | 1363 | } |
1359 | } | 1364 | } |
1360 | } | 1365 | } |
diff --git a/OpenSim/Region/Physics/BulletSPlugin/BSScene.cs b/OpenSim/Region/Physics/BulletSPlugin/BSScene.cs index 94a0ccf..417cb5f 100644 --- a/OpenSim/Region/Physics/BulletSPlugin/BSScene.cs +++ b/OpenSim/Region/Physics/BulletSPlugin/BSScene.cs | |||
@@ -52,6 +52,7 @@ using OpenSim.Region.Framework; | |||
52 | // Should prim.link() and prim.delink() membership checking happen at taint time? | 52 | // Should prim.link() and prim.delink() membership checking happen at taint time? |
53 | // Mesh sharing. Use meshHash to tell if we already have a hull of that shape and only create once | 53 | // Mesh sharing. Use meshHash to tell if we already have a hull of that shape and only create once |
54 | // Do attachments need to be handled separately? Need collision events. Do not collide with VolumeDetect | 54 | // Do attachments need to be handled separately? Need collision events. Do not collide with VolumeDetect |
55 | // Use collision masks for collision with terrain and phantom objects | ||
55 | // Implement the genCollisions feature in BulletSim::SetObjectProperties (don't pass up unneeded collisions) | 56 | // Implement the genCollisions feature in BulletSim::SetObjectProperties (don't pass up unneeded collisions) |
56 | // Implement LockAngularMotion | 57 | // Implement LockAngularMotion |
57 | // Decide if clearing forces is the right thing to do when setting position (BulletSim::SetObjectTranslation) | 58 | // Decide if clearing forces is the right thing to do when setting position (BulletSim::SetObjectTranslation) |
@@ -62,9 +63,6 @@ using OpenSim.Region.Framework; | |||
62 | // Multiple contact points on collision? | 63 | // Multiple contact points on collision? |
63 | // See code in ode::near... calls to collision_accounting_events() | 64 | // See code in ode::near... calls to collision_accounting_events() |
64 | // (This might not be a problem. ODE collects all the collisions with one object in one tick.) | 65 | // (This might not be a problem. ODE collects all the collisions with one object in one tick.) |
65 | // Use collision masks for collision with terrain and phantom objects | ||
66 | // Figure out how to not allocate a new Dictionary and List for every collision | ||
67 | // in BSPrim.Collide() and BSCharacter.Collide(). Can the same ones be reused? | ||
68 | // Raycast | 66 | // Raycast |
69 | // | 67 | // |
70 | namespace OpenSim.Region.Physics.BulletSPlugin | 68 | namespace OpenSim.Region.Physics.BulletSPlugin |
@@ -405,6 +403,8 @@ public class BSScene : PhysicsScene, IPhysicsParameters | |||
405 | // prevent simulation until we've been initialized | 403 | // prevent simulation until we've been initialized |
406 | if (!m_initialized) return 10.0f; | 404 | if (!m_initialized) return 10.0f; |
407 | 405 | ||
406 | long simulateStartTime = Util.EnvironmentTickCount(); | ||
407 | |||
408 | // update the prim states while we know the physics engine is not busy | 408 | // update the prim states while we know the physics engine is not busy |
409 | ProcessTaints(); | 409 | ProcessTaints(); |
410 | 410 | ||
@@ -437,13 +437,18 @@ public class BSScene : PhysicsScene, IPhysicsParameters | |||
437 | } | 437 | } |
438 | } | 438 | } |
439 | 439 | ||
440 | // The SendCollision's batch up the collisions on the objects. Now push the collisions into the simulator. | 440 | // The above SendCollision's batch up the collisions on the objects. |
441 | // Now push the collisions into the simulator. | ||
441 | foreach (BSPrim bsp in m_primsWithCollisions) | 442 | foreach (BSPrim bsp in m_primsWithCollisions) |
442 | bsp.SendCollisions(); | 443 | bsp.SendCollisions(); |
443 | m_primsWithCollisions.Clear(); | 444 | m_primsWithCollisions.Clear(); |
445 | |||
446 | // This is a kludge to get avatar movement updated. | ||
447 | // Don't send collisions only if there were collisions -- send everytime. | ||
448 | // ODE sends collisions even if there are none and this is used to update | ||
449 | // avatar animations and stuff. | ||
444 | // foreach (BSCharacter bsc in m_avatarsWithCollisions) | 450 | // foreach (BSCharacter bsc in m_avatarsWithCollisions) |
445 | // bsc.SendCollisions(); | 451 | // bsc.SendCollisions(); |
446 | // This is a kludge to get avatar movement updated. ODE sends collisions even if there isn't any | ||
447 | foreach (KeyValuePair<uint, BSCharacter> kvp in m_avatars) | 452 | foreach (KeyValuePair<uint, BSCharacter> kvp in m_avatars) |
448 | kvp.Value.SendCollisions(); | 453 | kvp.Value.SendCollisions(); |
449 | m_avatarsWithCollisions.Clear(); | 454 | m_avatarsWithCollisions.Clear(); |
@@ -465,10 +470,12 @@ public class BSScene : PhysicsScene, IPhysicsParameters | |||
465 | if (m_avatars.TryGetValue(entprop.ID, out actor)) | 470 | if (m_avatars.TryGetValue(entprop.ID, out actor)) |
466 | { | 471 | { |
467 | actor.UpdateProperties(entprop); | 472 | actor.UpdateProperties(entprop); |
473 | continue; | ||
468 | } | 474 | } |
469 | } | 475 | } |
470 | } | 476 | } |
471 | 477 | ||
478 | // If enabled, call into the physics engine to dump statistics | ||
472 | if (m_detailedStatsStep > 0) | 479 | if (m_detailedStatsStep > 0) |
473 | { | 480 | { |
474 | if ((m_simulationStep % m_detailedStatsStep) == 0) | 481 | if ((m_simulationStep % m_detailedStatsStep) == 0) |
@@ -477,6 +484,11 @@ public class BSScene : PhysicsScene, IPhysicsParameters | |||
477 | } | 484 | } |
478 | } | 485 | } |
479 | 486 | ||
487 | // this is a waste since the outside routine also calcuates the physics simulation | ||
488 | // period. TODO: There should be a way of computing physics frames from simulator computation. | ||
489 | // long simulateTotalTime = Util.EnvironmentTickCountSubtract(simulateStartTime); | ||
490 | // return (timeStep * (float)simulateTotalTime); | ||
491 | |||
480 | // TODO: FIX THIS: fps calculation wrong. This calculation always returns about 1 in normal operation. | 492 | // TODO: FIX THIS: fps calculation wrong. This calculation always returns about 1 in normal operation. |
481 | return timeStep / (numSubSteps * m_fixedTimeStep) * 1000f; | 493 | return timeStep / (numSubSteps * m_fixedTimeStep) * 1000f; |
482 | } | 494 | } |
@@ -528,6 +540,7 @@ public class BSScene : PhysicsScene, IPhysicsParameters | |||
528 | public override void SetWaterLevel(float baseheight) | 540 | public override void SetWaterLevel(float baseheight) |
529 | { | 541 | { |
530 | m_waterLevel = baseheight; | 542 | m_waterLevel = baseheight; |
543 | // TODO: pass to physics engine so things will float? | ||
531 | } | 544 | } |
532 | public float GetWaterLevel() | 545 | public float GetWaterLevel() |
533 | { | 546 | { |