//-----------------------------------------------------------------------------
// 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;
}
}
}
}