#region File Description //----------------------------------------------------------------------------- // Game.cs // // Microsoft XNA Community Game Platform // Copyright (C) Microsoft Corporation. All rights reserved. //----------------------------------------------------------------------------- #endregion #region Using Statements using System; using System.Collections.Generic; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Audio; using Microsoft.Xna.Framework.Content; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input; using Microsoft.Xna.Framework.Storage; #endregion namespace TransformedCollision { /// /// This is the main type for your game /// public class TransformedCollisionGame : Microsoft.Xna.Framework.Game { GraphicsDeviceManager graphics; // The images we will draw Texture2D personTexture; Texture2D blockTexture; // The color data for the images; used for per pixel collision Color[] personTextureData; Color[] blockTextureData; // The images will be drawn with this SpriteBatch SpriteBatch spriteBatch; // Person Vector2 personPosition; const int PersonMoveSpeed = 5; // Blocks List blocks = new List(); float BlockSpawnProbability = 0.01f; const int BlockFallSpeed = 1; const float BlockRotateSpeed = 0.005f; Vector2 blockOrigin; Random random = new Random(); // For when a collision is detected bool personHit = false; // The sub-rectangle of the drawable area which should be visible on all TVs Rectangle safeBounds; // Percentage of the screen on every side is the safe area const float SafeAreaPortion = 0.05f; public TransformedCollisionGame() { graphics = new GraphicsDeviceManager(this); Content.RootDirectory = "Content"; } /// /// Allows the game to perform any initialization it needs to before starting to /// run. This is where it can query for any required services and load any /// non-graphic related content. Calling base.Initialize will enumerate through /// any components and initialize them as well. /// protected override void Initialize() { base.Initialize(); // Calculate safe bounds based on current resolution Viewport viewport = graphics.GraphicsDevice.Viewport; safeBounds = new Rectangle( (int)(viewport.Width * SafeAreaPortion), (int)(viewport.Height * SafeAreaPortion), (int)(viewport.Width * (1 - 2 * SafeAreaPortion)), (int)(viewport.Height * (1 - 2 * SafeAreaPortion))); // Start the player in the center along the bottom of the screen personPosition.X = (safeBounds.Width - personTexture.Width) / 2; personPosition.Y = safeBounds.Height - personTexture.Height; } /// /// Load your graphics content. /// protected override void LoadContent() { // Load textures blockTexture = Content.Load("SpinnerBlock"); personTexture = Content.Load("Person"); // Extract collision data blockTextureData = new Color[blockTexture.Width * blockTexture.Height]; blockTexture.GetData(blockTextureData); personTextureData = new Color[personTexture.Width * personTexture.Height]; personTexture.GetData(personTextureData); // Calculate the block origin blockOrigin = new Vector2(blockTexture.Width / 2, blockTexture.Height / 2); // Create a sprite batch to draw those textures spriteBatch = new SpriteBatch(graphics.GraphicsDevice); } /// /// Allows the game to run logic such as updating the world, /// checking for collisions, gathering input and playing audio. /// /// Provides a snapshot of timing values. protected override void Update(GameTime gameTime) { // Get input KeyboardState keyboard = Keyboard.GetState(); GamePadState gamePad = GamePad.GetState(PlayerIndex.One); // Allows the game to exit if (gamePad.Buttons.Back == ButtonState.Pressed || keyboard.IsKeyDown(Keys.Escape)) { this.Exit(); } // Move the player left and right with arrow keys or d-pad if (keyboard.IsKeyDown(Keys.Left) || gamePad.DPad.Left == ButtonState.Pressed) { personPosition.X -= PersonMoveSpeed; } if (keyboard.IsKeyDown(Keys.Right) || gamePad.DPad.Right == ButtonState.Pressed) { personPosition.X += PersonMoveSpeed; } // Prevent the person from moving off of the screen personPosition.X = MathHelper.Clamp(personPosition.X, safeBounds.Left, safeBounds.Right - personTexture.Width); // Update the person's transform Matrix personTransform = Matrix.CreateTranslation(new Vector3(personPosition, 0.0f)); // Spawn new falling blocks if (random.NextDouble() < BlockSpawnProbability) { Block newBlock = new Block(); // at a random position just above the screen float x = (float)random.NextDouble() * (Window.ClientBounds.Width - blockTexture.Width); newBlock.Position = new Vector2(x, -blockTexture.Height); // with a random rotation newBlock.Rotation = (float)random.NextDouble() * MathHelper.TwoPi; blocks.Add(newBlock); } // Get the bounding rectangle of the person Rectangle personRectangle = new Rectangle((int)personPosition.X, (int)personPosition.Y, personTexture.Width, personTexture.Height); // Update each block personHit = false; for (int i = 0; i < blocks.Count; i++) { // Animate this block falling blocks[i].Position += new Vector2(0.0f, BlockFallSpeed); blocks[i].Rotation += BlockRotateSpeed; // Build the block's transform Matrix blockTransform = Matrix.CreateTranslation(new Vector3(-blockOrigin, 0.0f)) * // Matrix.CreateScale(block.Scale) * would go here Matrix.CreateRotationZ(blocks[i].Rotation) * Matrix.CreateTranslation(new Vector3(blocks[i].Position, 0.0f)); // Calculate the bounding rectangle of this block in world space Rectangle blockRectangle = CalculateBoundingRectangle( new Rectangle(0, 0, blockTexture.Width, blockTexture.Height), blockTransform); // The per-pixel check is expensive, so check the bounding rectangles // first to prevent testing pixels when collisions are impossible. if (personRectangle.Intersects(blockRectangle)) { // Check collision with person if (IntersectPixels(personTransform, personTexture.Width, personTexture.Height, personTextureData, blockTransform, blockTexture.Width, blockTexture.Height, blockTextureData)) { personHit = true; } } // Remove this block if it have fallen off the screen if (blocks[i].Position.Y > Window.ClientBounds.Height + blockOrigin.Length()) { blocks.RemoveAt(i); // When removing a block, the next block will have the same index // as the current block. Decrement i to prevent skipping a block. i--; } } base.Update(gameTime); } /// /// This is called when the game should draw itself. /// /// Provides a snapshot of timing values. protected override void Draw(GameTime gameTime) { GraphicsDevice device = graphics.GraphicsDevice; // Change the background to red when the person was hit by a block if (personHit) { device.Clear(Color.Red); } else { device.Clear(Color.CornflowerBlue); } spriteBatch.Begin(); // Draw person spriteBatch.Draw(personTexture, personPosition, Color.White); // Draw blocks foreach (Block block in blocks) { spriteBatch.Draw(blockTexture, block.Position, null, Color.White, block.Rotation, blockOrigin, 1.0f, SpriteEffects.None, 0.0f); } spriteBatch.End(); base.Draw(gameTime); } /// /// Determines if there is overlap of the non-transparent pixels /// between two sprites. /// /// Bounding rectangle of the first sprite /// Pixel data of the first sprite /// Bouding rectangle of the second sprite /// Pixel data of the second sprite /// True if non-transparent pixels overlap; false otherwise public static bool IntersectPixels(Rectangle rectangleA, Color[] dataA, Rectangle rectangleB, Color[] dataB) { // Find the bounds of the rectangle intersection int top = Math.Max(rectangleA.Top, rectangleB.Top); int bottom = Math.Min(rectangleA.Bottom, rectangleB.Bottom); int left = Math.Max(rectangleA.Left, rectangleB.Left); int right = Math.Min(rectangleA.Right, rectangleB.Right); // Check every point within the intersection bounds for (int y = top; y < bottom; y++) { for (int x = left; x < right; x++) { // Get the color of both pixels at this point Color colorA = dataA[(x - rectangleA.Left) + (y - rectangleA.Top) * rectangleA.Width]; Color colorB = dataB[(x - rectangleB.Left) + (y - rectangleB.Top) * rectangleB.Width]; // If both pixels are not completely transparent, if (colorA.A != 0 && colorB.A != 0) { // then an intersection has been found return true; } } } // No intersection found return false; } /// /// Determines if there is overlap of the non-transparent pixels between two /// sprites. /// /// World transform of the first sprite. /// Width of the first sprite's texture. /// Height of the first sprite's texture. /// Pixel color data of the first sprite. /// World transform of the second sprite. /// Width of the second sprite's texture. /// Height of the second sprite's texture. /// Pixel color data of the second sprite. /// True if non-transparent pixels overlap; false otherwise public static bool IntersectPixels( Matrix transformA, int widthA, int heightA, Color[] dataA, Matrix transformB, int widthB, int heightB, Color[] dataB) { // Calculate a matrix which transforms from A's local space into // world space and then into B's local space Matrix transformAToB = transformA * Matrix.Invert(transformB); // When a point moves in A's local space, it moves in B's local space with a // fixed direction and distance proportional to the movement in A. // This algorithm steps through A one pixel at a time along A's X and Y axes // Calculate the analogous steps in B: Vector2 stepX = Vector2.TransformNormal(Vector2.UnitX, transformAToB); Vector2 stepY = Vector2.TransformNormal(Vector2.UnitY, transformAToB); // Calculate the top left corner of A in B's local space // This variable will be reused to keep track of the start of each row Vector2 yPosInB = Vector2.Transform(Vector2.Zero, transformAToB); // For each row of pixels in A for (int yA = 0; yA < heightA; yA++) { // Start at the beginning of the row Vector2 posInB = yPosInB; // For each pixel in this row for (int xA = 0; xA < widthA; xA++) { // Round to the nearest pixel int xB = (int)Math.Round(posInB.X); int yB = (int)Math.Round(posInB.Y); // If the pixel lies within the bounds of B if (0 <= xB && xB < widthB && 0 <= yB && yB < heightB) { // Get the colors of the overlapping pixels Color colorA = dataA[xA + yA * widthA]; Color colorB = dataB[xB + yB * widthB]; // If both pixels are not completely transparent, if (colorA.A != 0 && colorB.A != 0) { // then an intersection has been found return true; } } // Move to the next pixel in the row posInB += stepX; } // Move to the next row yPosInB += stepY; } // No intersection found return false; } /// /// Calculates an axis aligned rectangle which fully contains an arbitrarily /// transformed axis aligned rectangle. /// /// Original bounding rectangle. /// World transform of the rectangle. /// A new rectangle which contains the trasnformed rectangle. public static Rectangle CalculateBoundingRectangle(Rectangle rectangle, Matrix transform) { // Get all four corners in local space Vector2 leftTop = new Vector2(rectangle.Left, rectangle.Top); Vector2 rightTop = new Vector2(rectangle.Right, rectangle.Top); Vector2 leftBottom = new Vector2(rectangle.Left, rectangle.Bottom); Vector2 rightBottom = new Vector2(rectangle.Right, rectangle.Bottom); // Transform all four corners into work space Vector2.Transform(ref leftTop, ref transform, out leftTop); Vector2.Transform(ref rightTop, ref transform, out rightTop); Vector2.Transform(ref leftBottom, ref transform, out leftBottom); Vector2.Transform(ref rightBottom, ref transform, out rightBottom); // Find the minimum and maximum extents of the rectangle in world space Vector2 min = Vector2.Min(Vector2.Min(leftTop, rightTop), Vector2.Min(leftBottom, rightBottom)); Vector2 max = Vector2.Max(Vector2.Max(leftTop, rightTop), Vector2.Max(leftBottom, rightBottom)); // Return that as a rectangle return new Rectangle((int)min.X, (int)min.Y, (int)(max.X - min.X), (int)(max.Y - min.Y)); } } }