//----------------------------------------------------------------------------- // CollisionSample.cs // // Microsoft XNA Community Game Platform // Copyright (C) Microsoft Corporation. All rights reserved. //----------------------------------------------------------------------------- using System; using System.Collections.Generic; using System.Linq; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Audio; using Microsoft.Xna.Framework.GamerServices; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input; using Microsoft.Xna.Framework.Input.Touch; using Microsoft.Xna.Framework.Media; namespace CollisionSample { /// /// This sample demonstrates various forms of collision detection for primitives /// supplied in the framework, plus oriented bounding boxes and triangles. /// public class CollisionSample : Microsoft.Xna.Framework.Game { #region Constants public const int FrustumGroupIndex = 0; public const int AABoxGroupIndex = 1; public const int OBoxGroupIndex = 2; public const int SphereGroupIndex = 3; public const int RayGroupIndex = 4; public const int NumGroups = 5; public const int TriIndex = 0; public const int SphereIndex = 1; public const int AABoxIndex = 2; public const int OBoxIndex = 3; public const int NumSecondaryShapes = 4; public const float CAMERA_SPACING = 50.0F; public const float YAW_RATE = 1; // radians per second for keyboard controls public const float PITCH_RATE = 0.75f; // radians per second for keyboard controls public const float YAW_DRAG_RATE = .01f; // radians per pixel for drag control public const float PITCH_DRAG_RATE = .01f; // radians per pixel for drag control public const float PINCH_ZOOM_RATE = .01f; // scale factor for pinch-zoom rate public const float DISTANCE_RATE = 10; #endregion #region Fields // Rendering helpers GraphicsDeviceManager graphics; DebugDraw debugDraw; // Primary shapes BoundingFrustum primaryFrustum; BoundingBox primaryAABox; BoundingOrientedBox primaryOBox; BoundingSphere primarySphere; Ray primaryRay; // Secondary shapes. Triangle[] secondaryTris = new Triangle[NumGroups]; BoundingSphere[] secondarySpheres = new BoundingSphere[NumGroups]; BoundingBox[] secondaryAABoxes = new BoundingBox[NumGroups]; BoundingOrientedBox[] secondaryOBoxes = new BoundingOrientedBox[NumGroups]; // Collision results ContainmentType[,] collideResults = new ContainmentType[NumGroups, NumSecondaryShapes]; Vector3? rayHitResult; // Camera state Vector3[] cameraOrigins = new Vector3[NumGroups]; int currentCamera; bool cameraOrtho; float cameraYaw; float cameraPitch; float cameraDistance; Vector3 cameraTarget; KeyboardState currentKeyboardState = new KeyboardState(); GamePadState currentGamePadState = new GamePadState(); List currentGestures = new List(); KeyboardState previousKeyboardState = new KeyboardState(); GamePadState previousGamePadState = new GamePadState(); TimeSpan unpausedClock = new TimeSpan(); bool paused; #endregion #region Initialization public CollisionSample() { graphics = new GraphicsDeviceManager(this); TouchPanel.EnabledGestures = GestureType.Tap | GestureType.Pinch | GestureType.FreeDrag; #if WINDOWS_PHONE TargetElapsedTime = TimeSpan.FromTicks(333333); graphics.IsFullScreen = true; #endif #if WINDOWS || XBOX graphics.PreferredBackBufferWidth = 853; graphics.PreferredBackBufferHeight = 480; #endif } // Set up initial bounding shapes for the primary (static) and secondary (moving) // bounding shapes along with relevant camera position information. protected override void Initialize() { Console.WriteLine("DEBUG - Game Initialize!"); debugDraw = new DebugDraw(GraphicsDevice); Components.Add(new FrameRateCounter(this)); // Primary frustum Matrix m1 = Matrix.CreatePerspectiveFieldOfView(MathHelper.PiOver4, 1.77778F, 0.5f, 10.0f); Matrix m2 = Matrix.CreateTranslation(new Vector3(0, 0, -7)); primaryFrustum = new BoundingFrustum(Matrix.Multiply(m2, m1)); cameraOrigins[FrustumGroupIndex] = Vector3.Zero; // Primary axis-aligned box primaryAABox.Min = new Vector3(CAMERA_SPACING - 3, -4, -5); primaryAABox.Max = new Vector3(CAMERA_SPACING + 3, 4, 5); cameraOrigins[AABoxGroupIndex] = new Vector3(CAMERA_SPACING, 0, 0); // Primary oriented box primaryOBox.Center = new Vector3(-CAMERA_SPACING, 0, 0); primaryOBox.HalfExtent = new Vector3(3, 4, 5); primaryOBox.Orientation = Quaternion.CreateFromYawPitchRoll(0.8f, 0.7f, 0); cameraOrigins[OBoxGroupIndex] = primaryOBox.Center; // Primary sphere primarySphere.Center = new Vector3(0, 0, -CAMERA_SPACING); primarySphere.Radius = 5; cameraOrigins[SphereGroupIndex] = primarySphere.Center; // Primary ray primaryRay.Position = new Vector3(0, 0, CAMERA_SPACING); primaryRay.Direction = Vector3.UnitZ; cameraOrigins[RayGroupIndex] = primaryRay.Position; // Initialize all of the secondary objects with default values Vector3 half = new Vector3(0.5F, 0.5F, 0.5F); for (int i = 0; i < NumGroups; i++) { secondarySpheres[i] = new BoundingSphere(Vector3.Zero, 1.0f); secondaryOBoxes[i] = new BoundingOrientedBox(Vector3.Zero, half, Quaternion.Identity); secondaryAABoxes[i] = new BoundingBox(-half, half); secondaryTris[i] = new Triangle(); } rayHitResult = null; currentCamera = 3; cameraOrtho = false; cameraYaw = (float)Math.PI * 0.75F; cameraPitch = MathHelper.PiOver4; cameraDistance = 20; cameraTarget = cameraOrigins[0]; paused = false; base.Initialize(); } #endregion #region Update protected override void Update(GameTime gameTime) { ReadInputDevices(); if (currentKeyboardState.IsKeyDown(Keys.Escape) || currentGamePadState.IsButtonDown(Buttons.Back)) this.Exit(); if (!paused) { unpausedClock += gameTime.ElapsedGameTime; } // Run collision even when paused, for timing and debugging, and so the step // forward/backward keys work. Animate(); Collide(); HandleInput(gameTime); base.Update(gameTime); } private void ReadInputDevices() { previousKeyboardState = currentKeyboardState; previousGamePadState = currentGamePadState; currentKeyboardState = Keyboard.GetState(); currentGamePadState = GamePad.GetState(PlayerIndex.One); currentGestures.Clear(); while (TouchPanel.IsGestureAvailable) { currentGestures.Add(TouchPanel.ReadGesture()); } } /// /// Move all the secondary shapes around, and move the ray so it /// sweeps back and forth across its secondry shapes. /// private void Animate() { float t = (float)unpausedClock.TotalSeconds / 2.0F; // Rates at which x,y,z cycle const float xRate = 1.1f; const float yRate = 3.6f; const float zRate = 1.9f; const float pathSize = 6; // how far apart following shapes are, in seconds const float gap = 0.25f; // orientation for rotatable shapes Quaternion orientation = Quaternion.CreateFromYawPitchRoll(t * 0.2f, t * 1.4f, t); for (int g = 0; g < NumGroups; g++) { // Animate spheres secondarySpheres[g].Center = cameraOrigins[g]; secondarySpheres[g].Center.X += pathSize * (float)Math.Sin(xRate * t); secondarySpheres[g].Center.Y += pathSize * (float)Math.Sin(yRate * t); secondarySpheres[g].Center.Z += pathSize * (float)Math.Sin(zRate * t); // Animate oriented boxes secondaryOBoxes[g].Center = cameraOrigins[g]; secondaryOBoxes[g].Orientation = orientation; secondaryOBoxes[g].Center.X += pathSize * (float)Math.Sin(xRate * (t - gap)); secondaryOBoxes[g].Center.Y += pathSize * (float)Math.Sin(yRate * (t - gap)); secondaryOBoxes[g].Center.Z += pathSize * (float)Math.Sin(zRate * (t - gap)); // Animate axis-aligned boxes Vector3 boxsize = new Vector3(1.0f, 1.3f, 1.9f); secondaryAABoxes[g].Min = cameraOrigins[g] - boxsize * 0.5f; secondaryAABoxes[g].Min.X += pathSize * (float)Math.Sin(xRate * (t - 2 * gap)); secondaryAABoxes[g].Min.Y += pathSize * (float)Math.Sin(yRate * (t - 2 * gap)); secondaryAABoxes[g].Min.Z += pathSize * (float)Math.Sin(zRate * (t - 2 * gap)); secondaryAABoxes[g].Max = secondaryAABoxes[g].Min + boxsize; // Animate triangles Vector3 trianglePos = cameraOrigins[g]; trianglePos.X += pathSize * (float)Math.Sin(xRate * (t - 3 * gap)); trianglePos.Y += pathSize * (float)Math.Sin(yRate * (t - 3 * gap)); trianglePos.Z += pathSize * (float)Math.Sin(zRate * (t - 3 * gap)); // triangle points in local space - equilateral triangle with radius of 2 secondaryTris[g].V0 = trianglePos + Vector3.Transform(new Vector3(0, 2, 0), orientation); secondaryTris[g].V1 = trianglePos + Vector3.Transform(new Vector3(1.73f, -1, 0), orientation); secondaryTris[g].V2 = trianglePos + Vector3.Transform(new Vector3(-1.73f, -1, 0), orientation); } //int index = (int)(t*5); //secondaryOBoxes[OBoxGroupIndex] = BoundingOrientedBox.iboxes[index % BoundingOrientedBox.iboxes.Length]; // Animate primary ray (this is the only animated primary object) // It sweeps back and forth across the secondary objects const float sweepTime = 3.1f; float rayDt = (-Math.Abs((t/sweepTime) % 2.0f - 1.0f) * NumSecondaryShapes + 0.5f) * gap; primaryRay.Direction.X = (float)Math.Sin(xRate * (t + rayDt)); primaryRay.Direction.Y = (float)Math.Sin(yRate * (t + rayDt)); primaryRay.Direction.Z = (float)Math.Sin(zRate * (t + rayDt)); primaryRay.Direction.Normalize(); } /// /// Check each pair of objects for collision/containment and store the results for /// coloring them at render time. /// private void Collide() { // test collisions between objects and frustum collideResults[FrustumGroupIndex, SphereIndex] = primaryFrustum.Contains(secondarySpheres[FrustumGroupIndex]); collideResults[FrustumGroupIndex, OBoxIndex] = BoundingOrientedBox.Contains(primaryFrustum, ref secondaryOBoxes[FrustumGroupIndex]); collideResults[FrustumGroupIndex, AABoxIndex] = primaryFrustum.Contains(secondaryAABoxes[FrustumGroupIndex]); collideResults[FrustumGroupIndex, TriIndex] = TriangleTest.Contains(primaryFrustum, ref secondaryTris[FrustumGroupIndex]); // test collisions between objects and aligned box collideResults[AABoxGroupIndex, SphereIndex] = primaryAABox.Contains(secondarySpheres[AABoxGroupIndex]); collideResults[AABoxGroupIndex, OBoxIndex] = BoundingOrientedBox.Contains(ref primaryAABox, ref secondaryOBoxes[AABoxGroupIndex]); collideResults[AABoxGroupIndex, AABoxIndex] = primaryAABox.Contains(secondaryAABoxes[AABoxGroupIndex]); collideResults[AABoxGroupIndex, TriIndex] = TriangleTest.Contains(ref primaryAABox, ref secondaryTris[AABoxGroupIndex]); // test collisions between objects and oriented box collideResults[OBoxGroupIndex, SphereIndex] = primaryOBox.Contains(ref secondarySpheres[OBoxGroupIndex]); collideResults[OBoxGroupIndex, OBoxIndex] = primaryOBox.Contains(ref secondaryOBoxes[OBoxGroupIndex]); collideResults[OBoxGroupIndex, AABoxIndex] = primaryOBox.Contains(ref secondaryAABoxes[OBoxGroupIndex]); collideResults[OBoxGroupIndex, TriIndex] = TriangleTest.Contains(ref primaryOBox, ref secondaryTris[OBoxGroupIndex]); // test collisions between objects and sphere collideResults[SphereGroupIndex, SphereIndex] = primarySphere.Contains(secondarySpheres[SphereGroupIndex]); collideResults[SphereGroupIndex, OBoxIndex] = BoundingOrientedBox.Contains(ref primarySphere, ref secondaryOBoxes[SphereGroupIndex]); collideResults[SphereGroupIndex, AABoxIndex] = primarySphere.Contains(secondaryAABoxes[SphereGroupIndex]); collideResults[SphereGroupIndex, TriIndex] = TriangleTest.Contains(ref primarySphere, ref secondaryTris[SphereGroupIndex]); // test collisions between objects and ray float dist = -1; collideResults[RayGroupIndex, SphereIndex] = collideResults[RayGroupIndex, OBoxIndex] = collideResults[RayGroupIndex, AABoxIndex] = collideResults[RayGroupIndex, TriIndex] = ContainmentType.Disjoint; rayHitResult = null; float? r = primaryRay.Intersects(secondarySpheres[RayGroupIndex]); if (r.HasValue) { collideResults[RayGroupIndex, SphereIndex] = ContainmentType.Intersects; dist = r.Value; } r = secondaryOBoxes[RayGroupIndex].Intersects(ref primaryRay); if (r.HasValue) { collideResults[RayGroupIndex, OBoxIndex] = ContainmentType.Intersects; dist = r.Value; } r = primaryRay.Intersects(secondaryAABoxes[RayGroupIndex]); if (r.HasValue) { collideResults[RayGroupIndex, AABoxIndex] = ContainmentType.Intersects; dist = r.Value; } r = TriangleTest.Intersects(ref primaryRay, ref secondaryTris[RayGroupIndex]); if (r.HasValue) { collideResults[RayGroupIndex, TriIndex] = ContainmentType.Intersects; dist = r.Value; } // If one of the ray intersection tests was successful, fDistance will be positive. // If so, compute the intersection location and store it in g_RayHitResultBox. if (dist > 0) { rayHitResult = primaryRay.Position + primaryRay.Direction * dist; } } private void HandleInput(GameTime gameTime) { // Rely on the fact that we are using fixed time-step update float dt = (float)gameTime.ElapsedGameTime.TotalSeconds; // Allow single-stepping through time if (paused) { if (currentKeyboardState.IsKeyDown(Keys.OemOpenBrackets)) unpausedClock -= TimeSpan.FromSeconds(dt); if (currentKeyboardState.IsKeyDown(Keys.OemCloseBrackets)) unpausedClock += TimeSpan.FromSeconds(dt); } // Change yaw/pitch based on right thumbstickb cameraYaw += currentGamePadState.ThumbSticks.Right.X * dt * YAW_RATE; cameraPitch += currentGamePadState.ThumbSticks.Right.Y * dt * PITCH_RATE; if (currentKeyboardState.IsKeyDown(Keys.Right)) cameraYaw += dt * YAW_RATE; if (currentKeyboardState.IsKeyDown(Keys.Left)) cameraYaw -= dt * YAW_RATE; if (currentKeyboardState.IsKeyDown(Keys.Up)) cameraPitch += dt * PITCH_RATE; if (currentKeyboardState.IsKeyDown(Keys.Down)) cameraPitch -= dt * PITCH_RATE; // Change distance based on right/left trigger if (currentGamePadState.IsButtonDown(Buttons.LeftTrigger) || currentKeyboardState.IsKeyDown(Keys.Subtract) || currentKeyboardState.IsKeyDown(Keys.OemMinus)) { cameraDistance += dt * DISTANCE_RATE; } if (currentGamePadState.IsButtonDown(Buttons.RightTrigger) || currentKeyboardState.IsKeyDown(Keys.Add) || currentKeyboardState.IsKeyDown(Keys.OemPlus)) { cameraDistance -= dt * DISTANCE_RATE; } // Group cycle if ( (currentKeyboardState.IsKeyDown(Keys.G) && previousKeyboardState.IsKeyUp(Keys.G)) || (currentGamePadState.IsButtonDown(Buttons.A) && previousGamePadState.IsButtonUp(Buttons.A))) { currentCamera = (currentCamera + 1) % NumGroups; } // Camera reset if ((currentKeyboardState.IsKeyDown(Keys.Home) && previousKeyboardState.IsKeyUp(Keys.Home)) || (currentGamePadState.IsButtonDown(Buttons.Y) && previousGamePadState.IsButtonUp(Buttons.Y))) { cameraYaw = (float)Math.PI * 0.75F; cameraPitch = MathHelper.PiOver4; cameraDistance = 40; } // Orthographic vs. perpsective projection toggle if ((currentKeyboardState.IsKeyDown(Keys.B) && previousKeyboardState.IsKeyUp(Keys.B)) || (currentGamePadState.IsButtonDown(Buttons.B) && previousGamePadState.IsButtonUp(Buttons.B))) { cameraOrtho = !cameraOrtho; } if (currentKeyboardState.IsKeyDown(Keys.O)) { cameraOrtho = true; } if (currentKeyboardState.IsKeyDown(Keys.P)) { cameraOrtho = false; } // Pause animation if ((currentKeyboardState.IsKeyDown(Keys.Space) && previousKeyboardState.IsKeyUp(Keys.Space)) || (currentGamePadState.IsButtonDown(Buttons.X) && previousGamePadState.IsButtonUp(Buttons.X))) { paused = !paused; } // Handle tap, drag, and pinch gestures foreach (GestureSample sample in currentGestures) { switch (sample.GestureType) { case GestureType.Tap: currentCamera = (currentCamera + 1) % NumGroups; break; case GestureType.FreeDrag: cameraYaw += sample.Delta.X * -YAW_DRAG_RATE; cameraPitch += sample.Delta.Y * PITCH_DRAG_RATE; break; case GestureType.Pinch: float dOld = Vector2.Distance(sample.Position - sample.Delta, sample.Position2 - sample.Delta2); float dNew = Vector2.Distance(sample.Position, sample.Position2); cameraDistance *= (float)Math.Exp((dOld - dNew) * PINCH_ZOOM_RATE); break; } } // Clamp camera to safe values cameraYaw = MathHelper.WrapAngle(cameraYaw); cameraPitch = MathHelper.Clamp(cameraPitch, -MathHelper.PiOver2, MathHelper.PiOver2); cameraDistance = MathHelper.Clamp(cameraDistance, 2, 80); // Handle time-based lerp for group transition float lerp = Math.Min(4.0F * dt, 1.0F); cameraTarget = (lerp * cameraOrigins[currentCamera]) + ((1.0F - lerp) * cameraTarget); } #endregion #region Draw /// /// This draws the bounding shapes and HUD for the sample. /// /// Provides a snapshot of timing values. protected override void Draw(GameTime gameTime) { GraphicsDevice.Clear(Color.CornflowerBlue); float aspect = GraphicsDevice.Viewport.AspectRatio; float yawCos = (float)Math.Cos(cameraYaw); float yawSin = (float)Math.Sin(cameraYaw); float pitchCos = (float)Math.Cos(cameraPitch); float pitchSin = (float)Math.Sin(cameraPitch); Vector3 eye = new Vector3(cameraDistance * pitchCos * yawSin + cameraTarget.X, cameraDistance * pitchSin + cameraTarget.Y, cameraDistance * pitchCos * yawCos + cameraTarget.Z); Matrix view = Matrix.CreateLookAt(eye, cameraTarget, Vector3.Up); Matrix projection = (cameraOrtho) ? Matrix.CreateOrthographic(aspect * cameraDistance, cameraDistance, 1.0F, 1000.0F) : Matrix.CreatePerspectiveFieldOfView((float)(System.Math.PI) / 4.0F, aspect, 1.0F, 1000.0F); debugDraw.Begin(view, projection); // Draw ground planes for (int g = 0; g < NumGroups; ++g) { Vector3 origin = new Vector3(cameraOrigins[g].X - 20, cameraOrigins[g].Y - 10, cameraOrigins[g].Z - 20); debugDraw.DrawWireGrid(Vector3.UnitX*40, Vector3.UnitZ*40, origin, 20, 20, Color.Black); } DrawPrimaryShapes(); // Draw secondary shapes for (int g = 0; g < NumGroups; g++) { debugDraw.DrawWireSphere(secondarySpheres[g], GetCollideColor(g, SphereIndex)); debugDraw.DrawWireBox(secondaryAABoxes[g], GetCollideColor(g, AABoxIndex)); debugDraw.DrawWireBox(secondaryOBoxes[g], GetCollideColor(g, OBoxIndex)); debugDraw.DrawWireTriangle(secondaryTris[g], GetCollideColor(g, TriIndex)); } // Draw results of ray-object intersection, if there was a hit this frame if (rayHitResult.HasValue) { Vector3 size = new Vector3(0.05f, 0.05f, 0.05f); BoundingBox weeBox = new BoundingBox(rayHitResult.Value - size, rayHitResult.Value + size); debugDraw.DrawWireBox(weeBox, Color.Yellow); } debugDraw.End(); // Draw overlay text. // string text = "A = (G)roup\nY = Reset (Home)\nB = (O)rtho/(P)erspective\nX = Pause (Space)"; // spriteBatch.Begin(); // spriteBatch.DrawString(spriteFont, text, new Vector2(86, 48), Color.White); // spriteBatch.End(); base.Draw(gameTime); } void DrawPrimaryShapes() { debugDraw.DrawWireBox(primaryAABox, Color.White); debugDraw.DrawWireBox(primaryOBox, Color.White); debugDraw.DrawWireFrustum(primaryFrustum, Color.White); debugDraw.DrawWireSphere(primarySphere, Color.White); debugDraw.DrawRay(primaryRay, Color.Red, 10.0f); } private Color GetCollideColor(int group, int shape) { ContainmentType cr = collideResults[group, shape]; switch (cr) { case ContainmentType.Contains: return Color.Red; case ContainmentType.Disjoint: return Color.LightGray; case ContainmentType.Intersects: return Color.Yellow; default: return Color.Black; } } #endregion } }