Friday, October 2, 2015

xna - Is there a simple way to group two or more sprites, so all of them will be dependent on each other?


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.


enter image description here


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 a List Children and provides a method to add new children.


  • Each Sprite knows how to calculate a Matrix LocalTransform which is defined relative to the parent.

  • Calling Draw on a Sprite 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:



enter image description here


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.



  1. Calculate global transform by multiplying the local transform by the parent's global transform.

  2. Adapt the global transform to SpriteBatch and render the current sprite.

  3. 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

Simple past, Present perfect Past perfect

Can you tell me which form of the following sentences is the correct one please? Imagine two friends discussing the gym... I was in a good s...