BulletSim: first version of raycast. Only single contact point and no

filtering.
BulletSim2017
Robert Adams 2017-09-03 17:15:27 -07:00
parent 6c5cfbafba
commit f348f7fa90
8 changed files with 140 additions and 131 deletions

View File

@ -121,6 +121,14 @@ public struct SweepHit
public float Fraction; public float Fraction;
public Vector3 Normal; public Vector3 Normal;
public Vector3 Point; public Vector3 Point;
public bool hasHit()
{
float sum = Fraction
+ Normal.X + Normal.Y + Normal.Z
+ Point.X + Point.Y + Point.Z;
return (sum != 0) || (ID != 0);
}
} }
[StructLayout(LayoutKind.Sequential)] [StructLayout(LayoutKind.Sequential)]
public struct RaycastHit public struct RaycastHit
@ -129,6 +137,12 @@ public struct RaycastHit
public float Fraction; public float Fraction;
public Vector3 Normal; public Vector3 Normal;
public Vector3 Point; public Vector3 Point;
public bool hasHit()
{
float sum = Normal.X + Normal.Y + Normal.Z + Point.X + Point.Y + Point.Z;
return (sum != 0);
}
} }
[StructLayout(LayoutKind.Sequential)] [StructLayout(LayoutKind.Sequential)]
public struct CollisionDesc public struct CollisionDesc

View File

@ -496,7 +496,6 @@ public sealed class BSCharacter : BSPhysObject
public override OMV.Vector3 ForceVelocity { public override OMV.Vector3 ForceVelocity {
get { return RawVelocity; } get { return RawVelocity; }
set { set {
PhysScene.AssertNotInSimulationTime("BSCharacter.ForceVelocity");
DetailLog("{0},BSCharacter.ForceVelocity.set={1}", LocalID, value); DetailLog("{0},BSCharacter.ForceVelocity.set={1}", LocalID, value);
RawVelocity = Util.ClampV(value, BSParam.MaxLinearVelocity); RawVelocity = Util.ClampV(value, BSParam.MaxLinearVelocity);
@ -638,8 +637,6 @@ public sealed class BSCharacter : BSPhysObject
public override float ForceBuoyancy { public override float ForceBuoyancy {
get { return _buoyancy; } get { return _buoyancy; }
set { set {
PhysScene.AssertNotInSimulationTime("BSCharacter.ForceBuoyancy");
_buoyancy = value; _buoyancy = value;
DetailLog("{0},BSCharacter.setForceBuoyancy,taint,buoy={1}", LocalID, _buoyancy); DetailLog("{0},BSCharacter.setForceBuoyancy,taint,buoy={1}", LocalID, _buoyancy);
// Buoyancy is faked by changing the gravity applied to the object // Buoyancy is faked by changing the gravity applied to the object

View File

@ -838,7 +838,7 @@ public static class BSParam
new ParameterDefn<float>("ResetBroadphasePool", "Setting this is any value resets the broadphase collision pool", new ParameterDefn<float>("ResetBroadphasePool", "Setting this is any value resets the broadphase collision pool",
0f, 0f,
(s) => { return 0f; }, (s) => { return 0f; },
(s,v) => { BSParam.ResetBroadphasePoolTainted(s, v, false /* inTaintTime */); } ), (s,v) => { BSParam.ResetBroadphasePoolTainted(s, v); } ),
new ParameterDefn<float>("ResetConstraintSolver", "Setting this is any value resets the constraint solver", new ParameterDefn<float>("ResetConstraintSolver", "Setting this is any value resets the constraint solver",
0f, 0f,
(s) => { return 0f; }, (s) => { return 0f; },
@ -924,10 +924,10 @@ public static class BSParam
// ===================================================================== // =====================================================================
// There are parameters that, when set, cause things to happen in the physics engine. // There are parameters that, when set, cause things to happen in the physics engine.
// This causes the broadphase collision cache to be cleared. // This causes the broadphase collision cache to be cleared.
private static void ResetBroadphasePoolTainted(BSScene pPhysScene, float v, bool inTaintTime) private static void ResetBroadphasePoolTainted(BSScene pPhysScene, float v)
{ {
BSScene physScene = pPhysScene; BSScene physScene = pPhysScene;
physScene.TaintedObject(inTaintTime, "BSParam.ResetBroadphasePoolTainted", delegate() physScene.TaintedObject(BSScene.DetailLogZero, "BSParam.ResetBroadphasePoolTainted", delegate()
{ {
physScene.PE.ResetBroadphasePool(physScene.World); physScene.PE.ResetBroadphasePool(physScene.World);
}); });

View File

@ -790,8 +790,6 @@ public class BSPrim : BSPhysObject
public override OMV.Vector3 ForceVelocity { public override OMV.Vector3 ForceVelocity {
get { return RawVelocity; } get { return RawVelocity; }
set { set {
PhysScene.AssertNotInSimulationTime("BSPrim.ForceVelocity");
RawVelocity = Util.ClampV(value, BSParam.MaxLinearVelocity); RawVelocity = Util.ClampV(value, BSParam.MaxLinearVelocity);
if (PhysBody.HasPhysicalBody) if (PhysBody.HasPhysicalBody)
{ {

View File

@ -81,7 +81,6 @@ public class BSPrimDisplaced : BSPrim
// Called at taint time. // Called at taint time.
public virtual Vector3 SetEffectiveCenterOfMassDisplacement(Vector3 centerOfMassDisplacement) public virtual Vector3 SetEffectiveCenterOfMassDisplacement(Vector3 centerOfMassDisplacement)
{ {
PhysScene.AssertInTaintTime("BSPrimDisplaced.SetEffectiveCenterOfMassDisplacement");
Vector3 comDisp; Vector3 comDisp;
if (UserSetCenterOfMassDisplacement.HasValue) if (UserSetCenterOfMassDisplacement.HasValue)
comDisp = (OMV.Vector3)UserSetCenterOfMassDisplacement; comDisp = (OMV.Vector3)UserSetCenterOfMassDisplacement;

View File

@ -124,10 +124,8 @@ namespace OpenSim.Region.PhysicsModule.BulletS
// True if initialized and ready to do simulation steps // True if initialized and ready to do simulation steps
private bool m_initialized = false; private bool m_initialized = false;
// Flag which is true when processing taints. // Object locked whenever execution is inside the physics engine
// Not guaranteed to be correct all the time (don't depend on this) but good for debugging. public Object PhysicsEngineLock = new object();
public bool InTaintTime { get; private set; }
// Flag that is true when the simulator is active and shouldn't be touched // Flag that is true when the simulator is active and shouldn't be touched
public bool InSimulationTime { get; private set; } public bool InSimulationTime { get; private set; }
@ -348,7 +346,6 @@ namespace OpenSim.Region.PhysicsModule.BulletS
m_log.InfoFormat("{0} Linksets implemented with {1}", LogHeader, (BSLinkset.LinksetImplementation)BSParam.LinksetImplementation); m_log.InfoFormat("{0} Linksets implemented with {1}", LogHeader, (BSLinkset.LinksetImplementation)BSParam.LinksetImplementation);
InSimulationTime = false; InSimulationTime = false;
InTaintTime = false;
m_initialized = true; m_initialized = true;
// If the physics engine runs on its own thread, start same. // If the physics engine runs on its own thread, start same.
@ -661,10 +658,14 @@ namespace OpenSim.Region.PhysicsModule.BulletS
int beforeTime = Util.EnvironmentTickCount(); int beforeTime = Util.EnvironmentTickCount();
int simTime = 0; int simTime = 0;
int numTaints = 0;
int numSubSteps = 0;
InTaintTime = true; lock (PhysicsEngineLock)
{
InSimulationTime = true;
// update the prim states while we know the physics engine is not busy // update the prim states while we know the physics engine is not busy
int numTaints = ProcessTaints(); numTaints += ProcessTaints();
// Some of the physical objects requre individual, pre-step calls // Some of the physical objects requre individual, pre-step calls
// (vehicles and avatar movement, in particular) // (vehicles and avatar movement, in particular)
@ -673,11 +674,6 @@ namespace OpenSim.Region.PhysicsModule.BulletS
// the prestep actions might have added taints // the prestep actions might have added taints
numTaints += ProcessTaints(); numTaints += ProcessTaints();
lock (_taintLock)
{
InSimulationTime = true;
}
// The following causes the unmanaged code to output ALL the values found in ALL the objects in the world. // The following causes the unmanaged code to output ALL the values found in ALL the objects in the world.
// Only enable this in a limited test world with few objects. // Only enable this in a limited test world with few objects.
if (m_physicsPhysicalDumpEnabled) if (m_physicsPhysicalDumpEnabled)
@ -685,7 +681,6 @@ namespace OpenSim.Region.PhysicsModule.BulletS
// step the physical world one interval // step the physical world one interval
m_simulationStep++; m_simulationStep++;
int numSubSteps = 0;
try try
{ {
numSubSteps = PE.PhysicsStep(World, timeStep, m_maxSubSteps, m_fixedTimeStep, out updatedEntityCount, out collidersCount); numSubSteps = PE.PhysicsStep(World, timeStep, m_maxSubSteps, m_fixedTimeStep, out updatedEntityCount, out collidersCount);
@ -704,11 +699,7 @@ namespace OpenSim.Region.PhysicsModule.BulletS
if (PhysicsMetricDumpFrames != 0 && ((m_simulationStep % PhysicsMetricDumpFrames) == 0)) if (PhysicsMetricDumpFrames != 0 && ((m_simulationStep % PhysicsMetricDumpFrames) == 0))
PE.DumpPhysicsStatistics(World); PE.DumpPhysicsStatistics(World);
lock (_taintLock)
{
InTaintTime = false;
InSimulationTime = false; InSimulationTime = false;
}
// Some actors want to know when the simulation step is complete. // Some actors want to know when the simulation step is complete.
TriggerPostStepEvent(timeStep); TriggerPostStepEvent(timeStep);
@ -716,6 +707,9 @@ namespace OpenSim.Region.PhysicsModule.BulletS
// In case there were any parameter updates that happened during the simulation step // In case there were any parameter updates that happened during the simulation step
numTaints += ProcessTaints(); numTaints += ProcessTaints();
InSimulationTime = false;
}
// Get a value for 'now' so all the collision and update routines don't have to get their own. // Get a value for 'now' so all the collision and update routines don't have to get their own.
SimulationNowTime = Util.EnvironmentTickCount(); SimulationNowTime = Util.EnvironmentTickCount();
@ -1040,20 +1034,39 @@ namespace OpenSim.Region.PhysicsModule.BulletS
} }
} }
public override List<ContactResult> RaycastWorld(Vector3 position, Vector3 direction, float length, int Count) public override List<ContactResult> RaycastWorld(Vector3 position, Vector3 direction, float length, int count)
{
return (List<ContactResult>)RaycastWorld(position, direction, length, count, RayFilterFlags.All);
}
public override object RaycastWorld(Vector3 position, Vector3 direction, float length, int count, RayFilterFlags filter)
{ {
List<ContactResult> ret = new List<ContactResult>(); List<ContactResult> ret = new List<ContactResult>();
if (BSParam.UseBulletRaycast) if (BSParam.UseBulletRaycast)
{ {
} DetailLog("{0},RaycastWorld,pos={1},dir={2},len={3},count={4},filter={5}",
return ret; DetailLogZero, position, direction, length, count, filter);
} // NOTE: locking ensures the physics engine is not executing.
// The caller might have to wait for the physics engine to finish.
public override object RaycastWorld(Vector3 position, Vector3 direction, float length, int Count, RayFilterFlags filter) lock (PhysicsEngineLock)
{ {
object ret = null; Vector3 posFrom = position;
if (BSParam.UseBulletRaycast) Vector3 posTo = Vector3.Normalize(direction) * length + position;
DetailLog("{0},RaycastWorld,RayTest2,from={1},to={2}",
DetailLogZero, posFrom, posTo);
RaycastHit hitInfo = PE.RayTest2(World, posFrom, posTo, 0xffff, 0xffff);
if (hitInfo.hasHit())
{ {
ContactResult result = new ContactResult();
result.Pos = hitInfo.Point;
result.Normal = hitInfo.Normal;
result.ConsumerID = hitInfo.ID;
result.Depth = hitInfo.Fraction;
ret.Add(result);
DetailLog("{0},RaycastWorld,hit,pos={1},norm={2},depth={3},id={4}",
DetailLogZero, result.Pos, result.Normal, result.Depth, result.ConsumerID);
}
}
} }
return ret; return ret;
} }
@ -1173,47 +1186,40 @@ namespace OpenSim.Region.PhysicsModule.BulletS
// Calls to the PhysicsActors can't directly call into the physics engine // Calls to the PhysicsActors can't directly call into the physics engine
// because it might be busy. We delay changes to a known time. // because it might be busy. We delay changes to a known time.
// We rely on C#'s closure to save and restore the context for the delegate. // We rely on C#'s closure to save and restore the context for the delegate.
public void TaintedObject(string pOriginator, string pIdent, TaintCallback pCallback) // NOTE: 'inTaintTime' is no longer used. This entry exists so all the calls don't have to be changed.
// public void TaintedObject(bool inTaintTime, String pIdent, TaintCallback pCallback)
// {
// TaintedObject(BSScene.DetailLogZero, pIdent, pCallback);
// }
// NOTE: 'inTaintTime' is no longer used. This entry exists so all the calls don't have to be changed.
public void TaintedObject(bool inTaintTime, uint pOriginator, String pIdent, TaintCallback pCallback)
{ {
TaintedObject(false /*inTaintTime*/, pOriginator, pIdent, pCallback); TaintedObject(m_physicsLoggingEnabled ? pOriginator.ToString() : BSScene.DetailLogZero, pIdent, pCallback);
} }
public void TaintedObject(uint pOriginator, String pIdent, TaintCallback pCallback) public void TaintedObject(uint pOriginator, String pIdent, TaintCallback pCallback)
{ {
TaintedObject(false /*inTaintTime*/, m_physicsLoggingEnabled ? pOriginator.ToString() : BSScene.DetailLogZero, pIdent, pCallback); TaintedObject(m_physicsLoggingEnabled ? pOriginator.ToString() : BSScene.DetailLogZero, pIdent, pCallback);
}
public void TaintedObject(bool inTaintTime, String pIdent, TaintCallback pCallback)
{
TaintedObject(inTaintTime, BSScene.DetailLogZero, pIdent, pCallback);
}
public void TaintedObject(bool inTaintTime, uint pOriginator, String pIdent, TaintCallback pCallback)
{
TaintedObject(inTaintTime, m_physicsLoggingEnabled ? pOriginator.ToString() : BSScene.DetailLogZero, pIdent, pCallback);
} }
// Sometimes a potentially tainted operation can be used in and out of taint time. // Sometimes a potentially tainted operation can be used in and out of taint time.
// This routine executes the command immediately if in taint-time otherwise it is queued. // This routine executes the command immediately if in taint-time otherwise it is queued.
public void TaintedObject(bool inTaintTime, string pOriginator, string pIdent, TaintCallback pCallback) public void TaintedObject(string pOriginator, string pIdent, TaintCallback pCallback)
{ {
if (!m_initialized) return; if (!m_initialized) return;
lock (_taintLock) { if (Monitor.TryEnter(PhysicsEngineLock))
if (inTaintTime || !InSimulationTime) { {
// If we can get exclusive access to the physics engine, just do the operation
pCallback(); pCallback();
Monitor.Exit(PhysicsEngineLock);
} }
else {
_taintOperations.Add(new TaintCallbackEntry(pOriginator, pIdent, pCallback));
}
}
/*
if (inTaintTime)
pCallback();
else else
{ {
// The physics engine is busy, queue the operation
lock (_taintLock) lock (_taintLock)
{ {
_taintOperations.Add(new TaintCallbackEntry(pOriginator, pIdent, pCallback)); _taintOperations.Add(new TaintCallbackEntry(pOriginator, pIdent, pCallback));
} }
} }
*/
} }
private void TriggerPreStepEvent(float timeStep) private void TriggerPreStepEvent(float timeStep)
@ -1236,6 +1242,7 @@ namespace OpenSim.Region.PhysicsModule.BulletS
// a callback into itself to do the actual property change. That callback is called // a callback into itself to do the actual property change. That callback is called
// here just before the physics engine is called to step the simulation. // here just before the physics engine is called to step the simulation.
// Returns the number of taints processed // Returns the number of taints processed
// NOTE: Called while PhysicsEngineLock is locked
public int ProcessTaints() public int ProcessTaints()
{ {
int ret = 0; int ret = 0;
@ -1245,6 +1252,7 @@ namespace OpenSim.Region.PhysicsModule.BulletS
} }
// Returns the number of taints processed // Returns the number of taints processed
// NOTE: Called while PhysicsEngineLock is locked
private int ProcessRegularTaints() private int ProcessRegularTaints()
{ {
int ret = 0; int ret = 0;
@ -1293,6 +1301,7 @@ namespace OpenSim.Region.PhysicsModule.BulletS
// Taints that happen after the normal taint processing but before the simulation step. // Taints that happen after the normal taint processing but before the simulation step.
// Returns the number of taints processed // Returns the number of taints processed
// NOTE: Called while PhysicsEngineLock is locked
private int ProcessPostTaintTaints() private int ProcessPostTaintTaints()
{ {
int ret = 0; int ret = 0;
@ -1322,19 +1331,6 @@ namespace OpenSim.Region.PhysicsModule.BulletS
} }
return ret; return ret;
} }
// Verify that things are being diddled when the physics engine is not running.
public bool AssertNotInSimulationTime(string whereFrom)
{
if (InSimulationTime)
{
DetailLog("{0},BSScene.AssertInTaintTime,IN SIMULATION TIME,Region={1},Where={2}", DetailLogZero, RegionName, whereFrom);
m_log.ErrorFormat("{0} IN SIMULATION TIME!! Region={1}, Where={2}", LogHeader, RegionName, whereFrom);
// Util.PrintCallStack(DetailLog);
}
return InSimulationTime;
}
#endregion // Taints #endregion // Taints
#region IPhysicsParameters #region IPhysicsParameters

View File

@ -75,8 +75,6 @@ public sealed class BSShapeCollection : IDisposable
// Called at taint-time. // Called at taint-time.
public bool GetBodyAndShape(bool forceRebuild, BulletWorld sim, BSPhysObject prim, PhysicalDestructionCallback bodyCallback) public bool GetBodyAndShape(bool forceRebuild, BulletWorld sim, BSPhysObject prim, PhysicalDestructionCallback bodyCallback)
{ {
m_physicsScene.AssertNotInSimulationTime("BSShapeCollection.GetBodyAndShape");
bool ret = false; bool ret = false;
// This lock could probably be pushed down lower but building shouldn't take long // This lock could probably be pushed down lower but building shouldn't take long
@ -346,8 +344,6 @@ public sealed class BSShapeCollection : IDisposable
if (!body.HasPhysicalBody) if (!body.HasPhysicalBody)
return; return;
m_physicsScene.AssertNotInSimulationTime("BSShapeCollection.DereferenceBody");
lock (m_collectionActivityLock) lock (m_collectionActivityLock)
{ {
if (DDetail) DetailLog("{0},BSShapeCollection.DereferenceBody,DestroyingBody,body={1}", body.ID, body); if (DDetail) DetailLog("{0},BSShapeCollection.DereferenceBody,DestroyingBody,body={1}", body.ID, body);

View File

@ -48,67 +48,76 @@ namespace OpenSim.Region.PhysicsModule.BulletS.Tests
// Documentation on attributes: http://www.nunit.org/index.php?p=attributes&r=2.6.1 // Documentation on attributes: http://www.nunit.org/index.php?p=attributes&r=2.6.1
// Documentation on assertions: http://www.nunit.org/index.php?p=assertions&r=2.6.1 // Documentation on assertions: http://www.nunit.org/index.php?p=assertions&r=2.6.1
BSScene PhysicsScene { get; set; } BSScene _physicsScene { get; set; }
BSPrim TargetSphere { get; set; } BSPrim _targetSphere { get; set; }
Vector3 TargetSpherePosition { get; set; } Vector3 _targetSpherePosition { get; set; }
float simulationTimeStep = 0.089f; float _simulationTimeStep = 0.089f;
uint _targetLocalID = 123;
[TestFixtureSetUp] [TestFixtureSetUp]
public void Init() public void Init()
{ {
Dictionary<string, string> engineParams = new Dictionary<string, string>(); Dictionary<string, string> engineParams = new Dictionary<string, string>();
engineParams.Add("UseBulletRaycast", "true"); engineParams.Add("UseBulletRaycast", "true");
PhysicsScene = BulletSimTestsUtil.CreateBasicPhysicsEngine(engineParams); _physicsScene = BulletSimTestsUtil.CreateBasicPhysicsEngine(engineParams);
PrimitiveBaseShape pbs = PrimitiveBaseShape.CreateSphere(); PrimitiveBaseShape pbs = PrimitiveBaseShape.CreateSphere();
Vector3 pos = new Vector3(100.0f, 100.0f, 50f); Vector3 pos = new Vector3(100.0f, 100.0f, 50f);
pos.Z = PhysicsScene.TerrainManager.GetTerrainHeightAtXYZ(pos) + 2f; _targetSpherePosition = pos;
TargetSpherePosition = pos;
Vector3 size = new Vector3(10f, 10f, 10f); Vector3 size = new Vector3(10f, 10f, 10f);
pbs.Scale = size; pbs.Scale = size;
Quaternion rot = Quaternion.Identity; Quaternion rot = Quaternion.Identity;
bool isPhys = false; bool isPhys = false;
uint localID = 123;
PhysicsScene.AddPrimShape("TargetSphere", pbs, pos, size, rot, isPhys, localID); _physicsScene.AddPrimShape("TargetSphere", pbs, pos, size, rot, isPhys, _targetLocalID);
TargetSphere = (BSPrim)PhysicsScene.PhysObjects[localID]; _targetSphere = (BSPrim)_physicsScene.PhysObjects[_targetLocalID];
// The actual prim shape creation happens at taint time // The actual prim shape creation happens at taint time
PhysicsScene.ProcessTaints(); _physicsScene.ProcessTaints();
} }
[TestFixtureTearDown] [TestFixtureTearDown]
public void TearDown() public void TearDown()
{ {
if (PhysicsScene != null) if (_physicsScene != null)
{ {
// The Dispose() will also free any physical objects in the scene // The Dispose() will also free any physical objects in the scene
PhysicsScene.Dispose(); _physicsScene.Dispose();
PhysicsScene = null; _physicsScene = null;
} }
} }
// There is a 10x10x10 sphere at <100,100,50> // There is a 10x10x10 sphere at <100,100,50>
// Shoot rays around the sphere and verify it hits and doesn't hit // Shoot rays around the sphere and verify it hits and doesn't hit
// TestCase parameters are <x,y,z> of start and <x,y,z> of end and expected result // TestCase parameters are <x,y,z> of start and <x,y,z> of end and expected result
[TestCase(20f, 20f, 50f, 50f, 50f, 50f, true)] // in front to sphere [TestCase(100f, 50f, 50f, 100f, 150f, 50f, true, "Pass through sphere from front")]
[TestCase(20f, 20f, 100f, 50f, 50f, 50f, true)] // from above to sphere [TestCase(50f, 100f, 50f, 150f, 100f, 50f, true, "Pass through sphere from side")]
[TestCase(50f, 50f, 50f, 150f, 150f, 50f, true)] // through sphere [TestCase(50f, 50f, 50f, 150f, 150f, 50f, true, "Pass through sphere diaginally")]
[TestCase(50f, 50f, 65f, 150f, 150f, 65f, false)] // pass over sphere [TestCase(100f, 100f, 100f, 100f, 100f, 20f, true, "Pass through sphere from above")]
public void RaycastAroundObject(float fromX, float fromY, float fromZ, float toX, float toY, float toZ, bool expected) { [TestCase(20f, 20f, 50f, 80f, 80f, 50f, false, "Not reach sphere")]
[TestCase(50f, 50f, 65f, 150f, 150f, 65f, false, "Passed over sphere")]
public void RaycastAroundObject(float fromX, float fromY, float fromZ, float toX, float toY, float toZ, bool expected, string msg) {
Vector3 fromPos = new Vector3(fromX, fromY, fromZ); Vector3 fromPos = new Vector3(fromX, fromY, fromZ);
Vector3 toPos = new Vector3(toX, toY, toZ); Vector3 toPos = new Vector3(toX, toY, toZ);
Vector3 direction = toPos - fromPos; Vector3 direction = toPos - fromPos;
float len = Vector3.Distance(fromPos, toPos); float len = Vector3.Distance(fromPos, toPos);
List<ContactResult> results = PhysicsScene.RaycastWorld(fromPos, direction, len, 1); List<ContactResult> results = _physicsScene.RaycastWorld(fromPos, direction, len, 1);
if (expected) { if (expected) {
Assert.True(results.Count > 0); // The test coordinates should generate a hit
Assert.True(results.Count != 0, msg + ": Did not return a hit but expected to.");
Assert.True(results.Count == 1, msg + ": Raycast returned not just one hit result.");
Assert.True(results[0].ConsumerID == _targetLocalID, msg + ": Raycast returned a collision object other than the target");
} }
else else
{ {
Assert.False(results.Count > 0); // The test coordinates should not generate a hit
if (results.Count > 0)
{
Assert.False(results.Count > 0, msg + ": Returned a hit at " + results[0].Pos.ToString());
}
} }
} }
} }