GOAL: Make grass move in response to a character's movement.
Step 1) Create your grass image(with at least 3 parts) and add it to Content in the solution explorer(.png or .dds). Maybe something like this:
(note: I have gridlines and Info window turned on so I can check where the sprites will be in my texture image)
Step 2) New Project: (Windows Game 4.0) calling it Grass
Step 3) Start with something like this:
Code:
namespace Grass
{
public class Game1 : Microsoft.Xna.Framework.Game
{
const int SCREENWIDTH = 800, SCREENHEIGHT = 600;
const bool FULLSCREEN = false;
GraphicsDeviceManager graphics;
SpriteBatch spriteBatch;
KeyboardState kb, old_kb; bool ignore_escape = false;
Texture2D texture;
DynamicGrass grass;
public Game1()
{
graphics = new GraphicsDeviceManager(this) { PreferredBackBufferWidth = SCREENWIDTH, PreferredBackBufferHeight = SCREENHEIGHT, IsFullScreen = FULLSCREEN };
Content.RootDirectory = "Content";
}
#region I N I T
protected override void Initialize()
{
spriteBatch = new SpriteBatch(GraphicsDevice);
base.Initialize();
}
#endregion
#region L O A D
protected override void LoadContent()
{
texture = Content.Load<Texture2D>("grass");
}
protected override void UnloadContent() { }
#endregion
#region U P D A T E
protected override void Update(GameTime gameTime)
{
kb=Keyboard.GetState();
if ((!ignore_escape)&&(Keypress(Keys.Escape))) this.Exit();
ignore_escape=false;
base.Update(gameTime);
}
bool Keypress(Keys k) { if ((kb.IsKeyDown(k))&&(old_kb.IsKeyUp(k))) return true; return false; }
bool Keydown(Keys k) { if (kb.IsKeyDown(k)) return true; return false; }
#endregion
#region D R A W
protected override void Draw(GameTime gameTime)
{
GraphicsDevice.Clear(Color.Black);
old_kb = kb;
base.Draw(gameTime);
}
#endregion
}
}
Step 4) There is no DynamicGrass class yet so right click in the solution explorer and add the "DynamicGrass" class and we'll start with the following:
Code:
namespace Grass
{
class DynamicGrass
{
GraphicsDevice gpu;
int screenW, screenH;
SpriteBatch spriteBatch;
Texture2D texture;
Sprite bigGrass, gras1, gras2, gras3;
public Vector2 pos;
public DynamicGrass(GraphicsDevice device, SpriteBatch _spriteBatch, Texture2D _texture)
{
spriteBatch = _spriteBatch;
texture = _texture;
gpu = device;
screenW = gpu.Viewport.Width;
screenH = gpu.Viewport.Height;
pos = new Vector2(screenW / 2 - 192, screenH / 2);
bigGrass = new Sprite(new Rectangle(0, 0, 384, 128), pos);
Rectangle rect1 = new Rectangle(0, 128, 48, 128);
gras1 = new Sprite(rect1, pos); rect1.X += 64;
gras2 = new Sprite(rect1, pos); rect1.X += 64;
gras3 = new Sprite(rect1, pos);
}
public void Draw(Vector2 position, float spacing)
{
int i=0;
pos = position;
spriteBatch.Draw(texture, pos, bigGrass.rect, Color.White);
float endPos = position.X + bigGrass.rect.Width;
do
{
if (i == 0) spriteBatch.Draw(texture, position, gras1.rect, Color.White);
else if (i == 1) spriteBatch.Draw(texture, position, gras2.rect, Color.White);
else if (i == 2) spriteBatch.Draw(texture, position, gras3.rect, Color.White);
position.X += spacing;
i++;
if (i>2) i=0;
} while (position.X < endPos);
}
}
}
Note: You may find some things like Rectangle or Spritebatch underlined. If you right click on them and go into resolve - you should see a "using"...bla bla... if you click on that it will add the necessary "using" at the top of the class module to include access to the necessary namespace.
Step 5)We don't actually have a Sprite class yet, so let's add the following class:
Code:
namespace Grass
{
class Sprite
{
public Rectangle rect;
public Vector2 pos;
public Vector2 vel;
public float size;
public float rot;
public float rot_vel;
public Vector2 origin;
public Color col;
public Sprite(Rectangle rectangle, Vector2 position, float scale = 1f, float rotate = 0f)
{
rect = rectangle;
pos = position;
vel = Vector2.Zero;
size = scale;
rot = rotate;
rot_vel = 0f;
origin = new Vector2(rect.Width/2, rect.Height/2);
col = Color.White;
}
}
}
Step 6) We'll want to be able to see the mouse for testing interaction. In Game1.cs in Initialize, add the following(before base.Initialize):
IsMouseVisible = true;
Also since we now have the classes we need you can uncomment the part in the Load function for creating the DynamicGrass instance:
grass = new DynamicGrass(GraphicsDevice, spriteBatch, texture);
Step 7) In Game1.cs, we will change Draw to the following:
Code:
protected override void Draw(GameTime gameTime)
{
GraphicsDevice.Clear(Color.Black);
spriteBatch.Begin();
grass.Draw(grass.pos, 32);
spriteBatch.End();
old_kb = kb;
base.Draw(gameTime);
}
Now if you run it, you should get something like this:
Step 8) Now we'll want to keep track of the mouse states and where it was in the previous frame so we can get a velocity of impact. To do this first we need to add these variables at the top of Game1.cs:
MouseState ms, old_ms;
Vector2 mp, old_mp;
Step 9) Now in the Update of Game1.cs, we get the mouse data, mouse velocity, and pass it to an Update in DynamicGrass (which we haven't made yet). The code should look like this:
Code:
protected override void Update(GameTime gameTime)
{
kb = Keyboard.GetState();
ms = Mouse.GetState(); mp = new Vector2(ms.X, ms.Y);
if ((!ignore_escape)&&(Keypress(Keys.Escape))) this.Exit();
ignore_escape=false;
Vector2 vel = mp - old_mp;
float radius = 5.0f;
grass.Update(grass.pos, mp, radius, vel);
base.Update(gameTime);
}
Note that we could place the grass anywhere instead of the default position(grass.pos).
Step 10: Change the draw function. Since Update now takes care of the draw position, we don't need to pass a position in draw and we also need to remember the old mouse data.
Code:
protected override void Draw(GameTime gameTime)
{
GraphicsDevice.Clear(Color.Black);
spriteBatch.Begin();
grass.Draw();
spriteBatch.End();
old_kb = kb;
old_ms = ms; old_mp = mp;
base.Draw(gameTime);
}
Step 11) In DynamicGrass.cs -- we need to add some variables for keeping track of more detailed information of the grass bits. So let's add these:
Code:
class DynamicGrass
{
GraphicsDevice gpu;
int screenW, screenH;
SpriteBatch spriteBatch;
Texture2D texture;
Rectangle bigGrass, gras1, gras2, gras3;
public Vector2 pos;
private static Random rnd;
Sprite[] grassBits;
int num_grass_bits;
public float radius;
public float spacing;
Step 12) Now to change the constructor so that instead of using Sprites where unnecessary, we'll just use Rectangles to store the source rects and use Sprites for the grass bits which require this kind of detail. I randomize the rotation of each grass bit to create some variety.
Code:
public DynamicGrass(GraphicsDevice device, SpriteBatch _spriteBatch, Texture2D _texture)
{
spriteBatch = _spriteBatch;
texture = _texture;
gpu = device;
screenW = gpu.Viewport.Width;
screenH = gpu.Viewport.Height;
pos = new Vector2(screenW / 2 - 192, screenH / 2);
bigGrass = new Rectangle(0, 0, 384, 128);
Rectangle rect1 = new Rectangle(0, 128, 48, 128);
gras1 = rect1; rect1.X += 64;
gras2 = rect1; rect1.X += 64;
gras3 = rect1;
rnd = new Random();
radius = rect1.Width/2;
spacing = 32;
float endPos = bigGrass.Width;
num_grass_bits = bigGrass.Width / (int)spacing;
grassBits = new Sprite[num_grass_bits + 1];
int i=0;
Vector2 p = Vector2.Zero;
Rectangle r;
do
{
int n=i%3;
r=gras1; if (n==1) r=gras2; else if (n==2) r=gras3;
grassBits[i] = new Sprite(r, p, 1f, (float)rnd.NextDouble() * 0.2f);
grassBits[i].origin.Y = grassBits[i].rect.Height - 4;
p.X += spacing;
i++;
} while (p.X < endPos);
}
Step 13) Let's add an Update method to DynamicGrass.cs. It takes the difference between the mouse position and the position of approximately where a collision with a grass bit could occur and uses that difference and the circular radius of each combined to see if the circles overlap (meaning a collision occured). To test this - we just change the color to red for now. (Note this is why the bounding box region test is initially commented out - to ensure testing works):
Code:
#region U P D A T E
public void Update(Vector2 grass_pos, Vector2 p, float col_rad, Vector2 vel)
{
Vector2 diff;
pos = grass_pos;
float radsqr, distsqr;
int i = 0;
do
{
grass_pos = pos + grassBits[i].pos+grassBits[i].origin/2;
diff = grass_pos - p;
distsqr = diff.LengthSquared();
radsqr = col_rad + radius;
radsqr *= radsqr;
if (distsqr <= radsqr)
{
grassBits[i].col = Color.Red;
}
else grassBits[i].col = Color.White;
i++;
} while (i < num_grass_bits);
}
#endregion
Step 14) Modify the draw function. The positions (and eventually rotations and such) are made in the constructor and updates so now we simply draw the bits like so:
Code:
public void Draw()
{
Vector2 grass_pos=pos;
spriteBatch.Draw(texture, pos, bigGrass, Color.White);
Sprite gb;
int i = 0;
do
{
gb = grassBits[i];
grass_pos = pos + gb.pos + gb.origin;
spriteBatch.Draw(texture, grass_pos, gb.rect, gb.col, gb.rot, gb.origin, gb.size, SpriteEffects.None, 0);
i++;
} while (i<num_grass_bits);
}
And now when you run the program you should see red collisions with mouse like so:
We'll need to make several changes to the update function. We'll change the spacing to 16 to make the grass look thicker. We'll also add a grass_length = 35; to the constructor (approximating the length of the grass from origin to somewhere almost at the tip). This will rotate around the origin (in update) for each grass piece so we can test collision with a rotated version of the grass bit.
In Update we'll also test the rectangle of the whole thing to see if we can skip any unnecessary detection tests. What we'll do in each loop (for each piece of grass) is create an origin point (v1) and use grass length and .rot to calculate the rotated tip point (v2).
Then we check if the two circles bounding the tip and mouse are overlapping and if they are we apply a rotation in the approximate direction of the mouse's velocity(vel) and scale it down with some huge number (ie: /300 [depending on how sensitive you want the response to be]).
Then we apply a dampener to the velocity that makes itself 80% of what it was in the previous frame and we reduce the grass rotation by 95% each from so it sort of springs back to its starting position. A lot of changes are required(mostly in Update) but I put comments in the code to help you. Here's the entire new DynamicGrass class:
Code:
namespace Grass
{
class DynamicGrass
{
GraphicsDevice gpu;
SpriteBatch spriteBatch;
Texture2D texture;
int screenW, screenH;
Rectangle bigGrass, gras1, gras2, gras3;
Sprite[] grassBits;
int num_grass_bits;
float grass_length;
public Vector2 pos;
public float radius;
public float spacing;
#region C O N S T R U C T O R
public DynamicGrass(GraphicsDevice device, SpriteBatch _spriteBatch, Texture2D _texture)
{
spriteBatch = _spriteBatch;
texture = _texture;
gpu = device;
screenW = gpu.Viewport.Width;
screenH = gpu.Viewport.Height;
pos = new Vector2(screenW / 2 - 192, screenH / 2);
bigGrass = new Rectangle(0, 0, 384, 128);
Rectangle rect1 = new Rectangle(0, 128, 48, 128);
gras1 = rect1; rect1.X += 64;
gras2 = rect1; rect1.X += 64;
gras3 = rect1;
radius = rect1.Width/2;
spacing = 16;
num_grass_bits = bigGrass.Width / (int)spacing;
grassBits = new Sprite[num_grass_bits + 1];
int i=0;
Vector2 p = Vector2.Zero;
float endPos = bigGrass.Width;
Rectangle r;
do
{
int n=i%3;
r=gras1; if (n==1) r=gras2; else if (n==2) r=gras3;
grassBits[i] = new Sprite(r, p, 1f, 0);
grassBits[i].origin.Y = grassBits[i].rect.Height/3*2;
p.X += spacing;
i++;
} while (p.X < endPos);
grass_length = 35;
}
#endregion
#region U P D A T E
public void Update(Vector2 grass_pos, Vector2 collider_pos, float collider_radius, Vector2 vel)
{
bool check_collisions = true;
if ((collider_pos.X < pos.X) || (collider_pos.X > pos.X + bigGrass.Width) ||
(collider_pos.Y < pos.Y) || (collider_pos.Y > pos.Y + bigGrass.Height)) check_collisions = false;
Vector2 diff, v1, v2;
pos = grass_pos;
float radsqr, distsqr;
int i = 0;
do
{
grassBits[i].col = Color.White;
if (check_collisions)
{
v1 = pos + grassBits[i].pos + grassBits[i].origin;
float true_rot = grassBits[i].rot + MathHelper.PiOver2;
v2.X = v1.X + grass_length * fCos(true_rot);
v2.Y = v1.Y - grass_length * fSin(true_rot);
diff = v2 - collider_pos;
distsqr = diff.LengthSquared();
radsqr = collider_radius + radius;
radsqr = radsqr * radsqr;
if (distsqr <= radsqr)
{
grassBits[i].col = Color.Red;
grassBits[i].rot_vel += vel.X / 300.0f;
}
}
grassBits[i].rot += grassBits[i].rot_vel;
if (grassBits[i].rot > 1.2f) {grassBits[i].rot = 1.2f; grassBits[i].rot_vel*=-1;}
if (grassBits[i].rot < -1.2f) { grassBits[i].rot = -1.2f; grassBits[i].rot_vel *= -1; }
grassBits[i].rot *= 0.95f;
grassBits[i].rot_vel *= 0.8f;
i++;
} while (i < num_grass_bits);
}
public static float fCos(float angle) { return (float)Math.Cos((float)angle); }
public static float fSin(float angle) { return (float)Math.Sin((float)angle); }
#endregion
#region D R A W
public void Draw()
{
Vector2 grass_pos=pos;
spriteBatch.Draw(texture, pos, bigGrass, Color.White);
Sprite gb;
int i = 0;
do
{
gb = grassBits[i];
grass_pos = pos + gb.pos + gb.origin;
spriteBatch.Draw(texture, grass_pos, gb.rect, gb.col, gb.rot, gb.origin, gb.size, SpriteEffects.None, 0);
i++;
} while (i<num_grass_bits);
}
#endregion
}
}
And now if you run it you will see the grass moving in response to mouse collisions - like in this picture:
Full Project Source
Home Page