I think that this question is very similar to this one, but I'm not sure if the answers are universal.
So, my goal is:
- Place two sprites in fixed position, for example player and his eyes
- Make sure that whenever the player rotates, eyes sprite rotates too and gets to the same relative position from the body (so eyes aren't on player's back). So they'll be working as a group. - This step should be automated, that's my goal!
So, for example, I now want to place a gun in user's hands. So now I say, that player is in position Vector2(0, 0)
and gun is in position Vector2(26, 16)
. Now I want to group them up, so whenever the player rotates, gun rotates too.
Currently in this example it's pretty okay but in case that I'd need to move my gun on axis y (only), I'm lost
Answer
Concept
I would solve this problem with a sprite hierarchy using a varitation of the composite design pattern. This means having each sprite store a list of the children sprites that are attached to it so that any modifications to the parent are automatically reflected in them (including translation, rotation and scaling).
In my engine I have implemented it like this:
- Each
Sprite
stores aList
and provides a method to add new children.Children - Each
Sprite
knows how to calculate aMatrix LocalTransform
which is defined relative to the parent. - Calling
Draw
on aSprite
also calls it on all of its children. - Children multiply their local transform by their parent's global transform. The result is what you use when rendering.
With this you'll be able to do what you asked for without any other modifications to your code. Here's an example:
Sprite tank = new Sprite(tankTexture);
tank.Children.Add(new Sprite(turretTexture) {Position = new Vector2(26, 16) });
spriteBatch.Begin();
tank.Draw(spriteBatch);
spriteBatch.End();
Implementation
For starters I'll just drop in a sample project with this technique implemented, in case you prefer to just look at the code and figure it out:
Note: I've opted for clarity instead of performance here. In a serious implementation, there are a lot of optimizations that could be done, most of which involve caching transforms and only recalculating them as needed (for instance, cache both local and global transforms at each sprite, and recalculate them only when the sprite or one of its ancestors changes). Also, the versions of XNA's matrix and vector operations that take values by reference are a bit faster than the ones I used here.
But I'll describe the process in more detail below, so read on for more information.
Step 1 - Make a few adjustments to the Sprite class
Assuming you already have a Sprite
class in place (and you should) then you'll need to make a few modifications to it. In particular you'll need to add the list of children sprites, the local transform matrix, and a way to propagate transforms down the sprite hierarchy. I found it easiest way to do that just to pass them as a parameter when drawing. Example:
public class Sprite
{
public Vector2 Position { get; set; }
public float Rotation { get; set; }
public Vector2 Scale { get; set; }
public Texture2D Texture { get; set; }
public List Children { get; }
public Matrix LocalTransform { get; }
public void Draw(SpriteBatch spriteBatch, Matrix parentTransform);
}
Step 2 - Calculating the LocalTransform matrix
The LocalTransform
matrix is just a regular world matrix built from the sprite's position, rotation and scale values. For the origin I assumed the center of the sprite:
public Matrix LocalTransform
{
get
{
// Transform = -Origin * Scale * Rotation * Translation
return Matrix.CreateTranslation(-Texture.Width/2f, -Texture.Height/2f, 0f) *
Matrix.CreateScale(Scale.X, Scale.Y, 1f) *
Matrix.CreateRotationZ(Rotation) *
Matrix.CreateTranslation(Position.X, Position.Y, 0f);
}
}
Step 3 - Knowing how to pass a Matrix to SpriteBatch
One problem with the SpriteBatch
class is that its Draw
method doesn't know how to take a world matrix directly. Here's a an helper method to bridge this problem:
public static void DecomposeMatrix(ref Matrix matrix, out Vector2 position, out float rotation, out Vector2 scale)
{
Vector3 position3, scale3;
Quaternion rotationQ;
matrix.Decompose(out scale3, out rotationQ, out position3);
Vector2 direction = Vector2.Transform(Vector2.UnitX, rotationQ);
rotation = (float) Math.Atan2(direction.Y, direction.X);
position = new Vector2(position3.X, position3.Y);
scale = new Vector2(scale3.X, scale3.Y);
}
Step 4 - Rendering the Sprite
Note: The Draw
method takes the parent's global transform as a parameter. There are other ways to propagate this information, but I found this one to be easy to use.
- Calculate global transform by multiplying the local transform by the parent's global transform.
- Adapt the global transform to
SpriteBatch
and render the current sprite. - Render all children passing them the current global transform as parameter.
Translating that into code you'll get something like:
public void Draw(SpriteBatch spriteBatch, Matrix parentTransform)
{
// Calculate global transform
Matrix globalTransform = LocalTransform * parentTransform;
// Get values from GlobalTransform for SpriteBatch and render sprite
Vector2 position, scale;
float rotation;
DecomposeMatrix(ref globalTransform, out position, out rotation, out scale);
spriteBatch.Draw(Texture, position, null, Color.White, rotation, Vector2.Zero, scale, SpriteEffects.None, 0.0f);
// Draw Children
Children.ForEach(c => c.Draw(spriteBatch, globalTransform));
}
When drawing the root sprite there's no parent transform, so you pass it Matrix.Identity
. You can create an overload to help with this case:
public void Draw(SpriteBatch spriteBatch) { Draw(spriteBatch, Matrix.Identity); }
No comments:
Post a Comment