Concepto
Solucionaría este problema con una jerarquía de sprites usando una variación del patrón de diseño compuesto . Esto significa que cada sprite almacene una lista de los sprites secundarios que están adjuntos para que cualquier modificación en el padre se refleje automáticamente en ellos (incluida la traducción, rotación y escalado).
En mi motor lo he implementado así:
- Cada uno
Sprite
almacena List<Sprite> Children
y proporciona un método para agregar nuevos hijos.
- Cada uno
Sprite
sabe cómo calcular un Matrix LocalTransform
que se define en relación con el padre.
- Llamar
Draw
a un Sprite
también lo llama a todos sus hijos.
- Los niños multiplican su transformación local por la transformación global de sus padres . El resultado es lo que usa al renderizar.
Con esto, podrá hacer lo que solicitó sin ninguna otra modificación en su código. Aquí hay un ejemplo:
Sprite tank = new Sprite(tankTexture);
tank.Children.Add(new Sprite(turretTexture) {Position = new Vector2(26, 16) });
spriteBatch.Begin();
tank.Draw(spriteBatch);
spriteBatch.End();
Implementación
Para empezar, solo agregaré un proyecto de muestra con esta técnica implementada, en caso de que prefiera mirar el código y descifrarlo:
Nota: He optado por la claridad en lugar del rendimiento aquí. En una implementación seria, hay muchas optimizaciones que podrían hacerse, la mayoría de las cuales implican el almacenamiento en caché de las transformaciones y solo recalcularlas según sea necesario (por ejemplo, almacenar en caché las transformaciones locales y globales en cada sprite, y recalcularlas solo cuando el sprite o uno de sus antepasados cambia). Además, las versiones de las operaciones de matriz y vector de XNA que toman valores por referencia son un poco más rápidas que las que utilicé aquí.
Pero describiré el proceso con más detalle a continuación, así que siga leyendo para obtener más información.
Paso 1: haz algunos ajustes en la clase Sprite
Suponiendo que ya tiene una Sprite
clase en su lugar (y debería hacerlo), deberá realizar algunas modificaciones. En particular, deberá agregar la lista de sprites secundarios, la matriz de transformación local y una forma de propagar transformaciones en la jerarquía de sprites. Encontré que la forma más fácil de hacerlo es pasarlos como un parámetro al dibujar. Ejemplo:
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<Sprite> Children { get; }
public Matrix LocalTransform { get; }
public void Draw(SpriteBatch spriteBatch, Matrix parentTransform);
}
Paso 2 - Cálculo de la matriz LocalTransform
La LocalTransform
matriz es solo una matriz mundial regular construida a partir de los valores de posición, rotación y escala del sprite. Para el origen, asumí el centro del 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);
}
}
Paso 3: saber cómo pasar una matriz a SpriteBatch
Un problema con la SpriteBatch
clase es que su Draw
método no sabe cómo tomar una matriz mundial directamente. Aquí hay un método auxiliar para salvar este problema:
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);
}
Paso 4 - Renderizando el Sprite
Nota: El Draw
método toma la transformación global del padre como parámetro. Hay otras formas de propagar esta información, pero esta me pareció fácil de usar.
- Calcule la transformación global multiplicando la transformación local por la transformación global del padre.
- Adapte la transformación global
SpriteBatch
y renderice el sprite actual.
- Haga que todos los hijos pasen la transformación global actual como parámetro.
Traduciendo eso en código obtendrás algo como:
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));
}
Al dibujar el sprite raíz no hay transformación principal, por lo que debe pasarla Matrix.Identity
. Puede crear una sobrecarga para ayudar con este caso:
public void Draw(SpriteBatch spriteBatch) { Draw(spriteBatch, Matrix.Identity); }