//----------------------------------------------------------------------------- // FlockingSample.cs // // Microsoft XNA Community Game Platform // Copyright (C) Microsoft Corporation. All rights reserved. //----------------------------------------------------------------------------- using System; using System.Collections.Generic; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Content; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input; using Microsoft.Xna.Framework.Input.Touch; namespace Flocking { public struct AIParameters { /// /// how far away the animals see each other /// public float DetectionDistance; /// /// seperate from animals inside this distance /// public float SeparationDistance; /// /// how much the animal tends to move in it's previous direction /// public float MoveInOldDirectionInfluence; /// /// how much the animal tends to move with animals in it's detection distance /// public float MoveInFlockDirectionInfluence; /// /// how much the animal tends to move randomly /// public float MoveInRandomDirectionInfluence; /// /// how quickly the animal can turn /// public float MaxTurnRadians; /// /// how much each nearby animal influences it's behavior /// public float PerMemberWeight; /// /// how much dangerous animals influence it's behavior /// public float PerDangerWeight; } /// /// This is the main type for your game /// public class FlockingSample : Microsoft.Xna.Framework.Game { // X location to start drawing the HUD from const int hudLocX = 200; // Y location to start drawing the HUD from const int hudLocY = 30; // Min value for the distance sliders const float sliderMin = 0.0f; // Max value for the distance sliders const float sliderMax = 100f; // Width of the slider button const int sliderButtonWidth = 10; // Default value for the AI parameters const float detectionDefault = 70.0f; const float separationDefault = 50.0f; const float moveInOldDirInfluenceDefault = 1.0f; const float moveInFlockDirInfluenceDefault = 1.0f; const float moveInRandomDirInfluenceDefault = 0.05f; const float maxTurnRadiansDefault = 6.0f; const float perMemberWeightDefault = 1.0f; const float perDangerWeightDefault = 50.0f; GraphicsDeviceManager graphics; SpriteBatch spriteBatch; InputState inputState; SpriteFont hudFont; // Do we need to update AI parameers this Update bool aiParameterUpdate = false; bool moveCat = false; Texture2D bButton; Texture2D xButton; Texture2D yButton; Texture2D onePixelWhite; Texture2D birdTexture; Texture2D catTexture; Cat cat; Flock flock; AIParameters flockParams; // Definte the dimensions of the controls Rectangle barDetectionDistance = new Rectangle(205, 45, 85, 40); Rectangle barSeparationDistance = new Rectangle(205, 125, 85, 40); Rectangle buttonResetDistance = new Rectangle(hudLocX + 110, hudLocY, 140, 40); Rectangle buttonResetFlock = new Rectangle(hudLocX + 110, hudLocY + 20, 140, 40); Rectangle buttonToggleCat = new Rectangle(hudLocX + 110, hudLocY + 40, 140, 40); int selectionNum; private bool lastMousePressed; public FlockingSample() { graphics = new GraphicsDeviceManager(this); graphics.SupportedOrientations = DisplayOrientation.Portrait; graphics.PreferredBackBufferWidth = 600; graphics.PreferredBackBufferHeight = 720; Content.RootDirectory = "Content"; IsMouseVisible = true; #if ___MOBILE___ graphics.IsFullScreen = true; #endif inputState = new InputState(); flock = null; cat = null; flockParams = new AIParameters(); ResetAIParams(); } /// /// 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() { // Enable the gestures we care about. you must set EnabledGestures before // you can use any of the other gesture APIs. TouchPanel.EnabledGestures = GestureType.Tap | GestureType.FreeDrag; base.Initialize(); } /// /// LoadContent will be called once per game and is the place to load /// all of your content. /// protected override void LoadContent() { spriteBatch = new SpriteBatch(GraphicsDevice); catTexture = Content.Load("cat"); birdTexture = Content.Load("mouse"); bButton = Content.Load("xboxControllerButtonB"); xButton = Content.Load("xboxControllerButtonX"); yButton = Content.Load("xboxControllerButtonY"); hudFont = Content.Load("HUDFont"); onePixelWhite = new Texture2D( GraphicsDevice, 1, 1, false, SurfaceFormat.Color); onePixelWhite.SetData(new Color[] { Color.White }); } /// /// Handles input for quitting the game. /// void HandleInput() { inputState.Update(); // Check for exit. if (inputState.Exit) { Exit(); } float dragDelta = 0f; // Check to see whether the user wants to modify their currently selected // weight. if (inputState.Up) { selectionNum--; if (selectionNum < 0) selectionNum = 1; } if (inputState.Down) { selectionNum = (selectionNum + 1) % 2; } // Update move for the cat if (cat != null) { cat.HandleInput(inputState); } // Turn the cat on or off if (inputState.ToggleCatButton) { ToggleCat(); } // Resets flock parameters back to default if (inputState.ResetDistances) { ResetAIParams(); aiParameterUpdate = true; } // Resets the location and orientation of the members of the flock if (inputState.ResetFlock) { flock.ResetFlock(); aiParameterUpdate = true; } dragDelta = inputState.SliderMove; // Apply to the changeAmount to the currentlySelectedWeight switch (selectionNum) { case 0: flockParams.DetectionDistance += dragDelta; break; case 1: flockParams.SeparationDistance += dragDelta; break; default: break; } if (dragDelta != 0f) aiParameterUpdate = true; // By default we can move the cat but if a touch or mouse registers against a control do not move the cat moveCat = true; TouchCollection rawTouch = TouchPanel.GetState(); // Use raw touch for the sliders if (rawTouch.Count > 0) { // Only grab the first one TouchLocation touchLocation = rawTouch[0]; // Create a collidable rectangle to determine if we touched the controls Rectangle touchRectangle = new Rectangle((int)touchLocation.Position.X, (int)touchLocation.Position.Y, 20, 20); // Have the sliders rely on the raw touch to function properly SliderInputHelper(touchRectangle); } // Handle gestures (taps) while (TouchPanel.IsGestureAvailable) { GestureSample gesture = TouchPanel.ReadGesture(); Rectangle touch = new Rectangle((int)gesture.Position.X, (int)gesture.Position.Y, 20, 20); switch (gesture.GestureType) { case GestureType.Tap: HandleButtonTap(touch); break; } if (cat != null && moveCat) { cat.Location = gesture.Position; } } // Mouse input for desktop MouseState mouse = Mouse.GetState(); Rectangle mouseRect = new Rectangle(mouse.X, mouse.Y, 1, 1); bool mousePressed = mouse.LeftButton == ButtonState.Pressed; bool mouseJustPressed = mousePressed && lastMousePressed == false; lastMousePressed = mousePressed; // Sliders: click or drag if (mousePressed) { if (barDetectionDistance.Intersects(mouseRect)) { selectionNum = 0; aiParameterUpdate = true; moveCat = false; flockParams.DetectionDistance = mouse.X - barDetectionDistance.X; } else if (barSeparationDistance.Intersects(mouseRect)) { selectionNum = 1; aiParameterUpdate = true; moveCat = false; flockParams.SeparationDistance = mouse.X - barDetectionDistance.X; } } // Buttons: single click if (mouseJustPressed) { if (HandleButtonTap(mouseRect)) { moveCat = false; } } // Move cat by clicking anywhere else if (cat != null && moveCat && mouseJustPressed) { cat.Location = new Vector2(mouse.X, mouse.Y); } // Clamp the slider values flockParams.DetectionDistance = MathHelper.Clamp(flockParams.DetectionDistance, sliderMin, sliderMax); flockParams.SeparationDistance = MathHelper.Clamp(flockParams.SeparationDistance, sliderMin, sliderMax); if (aiParameterUpdate) { flock.FlockParams = flockParams; } } // Shared button tap handler for both touch and mouse private bool HandleButtonTap(Rectangle tapRect) { if (buttonResetDistance.Intersects(tapRect)) { ResetAIParams(); aiParameterUpdate = true; return true; } else if (buttonResetFlock.Intersects(tapRect)) { flock.ResetFlock(); aiParameterUpdate = true; return true; } else if (buttonToggleCat.Intersects(tapRect)) { ToggleCat(); return true; } return false; } /// /// Helper function that handles Slider interaction logic /// /// Rectangle representing a touch private void SliderInputHelper( Rectangle touchRectangle) { if (barDetectionDistance.Intersects(touchRectangle)) { selectionNum = 0; aiParameterUpdate = true; moveCat = false; flockParams.DetectionDistance = touchRectangle.X - barDetectionDistance.X; } else if (barSeparationDistance.Intersects(touchRectangle)) { selectionNum = 1; aiParameterUpdate = true; moveCat = false; flockParams.SeparationDistance = touchRectangle.X - barDetectionDistance.X; } } /// /// 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) { HandleInput(); if (cat != null) { cat.Update(gameTime); } if (flock != null) { flock.Update(gameTime, cat); } else { SpawnFlock(); } base.Update(gameTime); } /// /// This is called when the game should draw itself. /// /// Provides a snapshot of timing values. protected override void Draw(GameTime gameTime) { graphics.GraphicsDevice.Clear(Color.CornflowerBlue); spriteBatch.Begin(); if (flock != null) { flock.Draw(spriteBatch, gameTime); } if (cat != null) { cat.Draw(spriteBatch, gameTime); } // Draw all the HUD elements DrawBar(barDetectionDistance, flockParams.DetectionDistance / 100f, "Detection Distance:", gameTime, selectionNum == 0); DrawBar(barSeparationDistance, flockParams.SeparationDistance / 100f, "Separation Distance:", gameTime, selectionNum == 1); spriteBatch.Draw(bButton, new Vector2(hudLocX + 110.0f, hudLocY), Color.White); spriteBatch.Draw(xButton, new Vector2(hudLocX + 110.0f, hudLocY + 20.0f), Color.White); spriteBatch.Draw(yButton, new Vector2(hudLocX + 110.0f, hudLocY + 40.0f), Color.White); spriteBatch.DrawString(hudFont, "Reset Distances", new Vector2(hudLocX + 135.0f, hudLocY), Color.White); spriteBatch.DrawString(hudFont, "Reset flock", new Vector2(hudLocX + 135.0f, hudLocY + 20.0f), Color.White); spriteBatch.DrawString(hudFont, "Spawn/remove cat", new Vector2(hudLocX + 135.0f, hudLocY + 40.0f), Color.White); #if DEBUG spriteBatch.DrawString(hudFont, $"Mouse: {Mouse.GetState().Position.ToString()}", new Vector2(hudLocX + 135.0f, hudLocY + 60.0f), Color.White); #endif spriteBatch.End(); base.Draw(gameTime); } /// /// Helper function used by Draw. It is used to draw the buttons /// /// /// private void DrawButton(Rectangle button, string label) { spriteBatch.Draw(onePixelWhite, button, Color.Orange); spriteBatch.DrawString(hudFont, label, new Vector2(button.Left + 10, button.Top + 10), Color.Black); } /// /// Helper function used by Draw. It is used to draw the slider bars /// private void DrawBar(Rectangle bar, float barWidthNormalized, string label, GameTime gameTime, bool highlighted) { Color tintColor = Color.White; // If the bar is highlighted, we want to make it pulse with a red tint. if (highlighted) { // To do this, we'll first generate a value t, which we'll use to // determine how much tint to have. float t = (float)Math.Sin(10 * gameTime.TotalGameTime.TotalSeconds); // Sin varies from -1 to 1, and we want t to go from 0 to 1, so we'll // scale it now. t = .5f + .5f * t; // Finally, we'll calculate our tint color by using Lerp to generate // a color in between Red and White. tintColor = new Color(Vector4.Lerp( Color.Red.ToVector4(), Color.White.ToVector4(), t)); } // Calculate how wide the bar should be, and then draw it. bar.Height /= 2; spriteBatch.Draw(onePixelWhite, bar, Color.White); // Draw the slider spriteBatch.Draw(onePixelWhite, new Rectangle(bar.X + (int)(bar.Width * barWidthNormalized), bar.Y - bar.Height / 2, sliderButtonWidth, bar.Height * 2), Color.Orange); // Finally, draw the label to the left of the bar. Vector2 labelSize = hudFont.MeasureString(label); Vector2 labelPosition = new Vector2(bar.X - 5 - labelSize.X, bar.Y); spriteBatch.DrawString(hudFont, label, labelPosition, tintColor); } /// /// Create the bird flock /// /// protected void SpawnFlock() { if (flock == null) { flock = new Flock(birdTexture, GraphicsDevice.Viewport.TitleSafeArea.Width, GraphicsDevice.Viewport.TitleSafeArea.Height, flockParams); } } /// /// Reset flock AI parameters /// private void ResetAIParams() { flockParams.DetectionDistance = detectionDefault; flockParams.SeparationDistance = separationDefault; flockParams.MoveInOldDirectionInfluence = moveInOldDirInfluenceDefault; flockParams.MoveInFlockDirectionInfluence = moveInFlockDirInfluenceDefault; flockParams.MoveInRandomDirectionInfluence = moveInRandomDirInfluenceDefault; flockParams.MaxTurnRadians = maxTurnRadiansDefault; flockParams.PerMemberWeight = perMemberWeightDefault; flockParams.PerDangerWeight = perDangerWeightDefault; } /// /// Create or remove the cat /// protected void ToggleCat() { if (cat == null) { cat = new Cat(catTexture, GraphicsDevice.Viewport.TitleSafeArea.Width, GraphicsDevice.Viewport.TitleSafeArea.Height); } else { cat = null; } } } }