Concetto
Vorrei risolvere questo problema con una gerarchia di sprite usando una variante del modello di disegno composito . Ciò significa che ogni sprite memorizza un elenco degli sprite secondari ad esso collegati in modo che qualsiasi modifica al genitore si rifletta automaticamente in essi (inclusi traduzione, rotazione e ridimensionamento).
Nel mio motore l'ho implementato in questo modo:
- Ciascuno
Sprite
memorizza un List<Sprite> Children
e fornisce un metodo per aggiungere nuovi figli.
- Ognuno
Sprite
sa come calcolare un Matrix LocalTransform
che è definito rispetto al genitore.
- Chiamare
Draw
a Sprite
lo chiama anche su tutti i suoi figli.
- I bambini moltiplicano la loro trasformazione locale per la trasformazione globale dei genitori . Il risultato è quello che usi durante il rendering.
Con questo sarai in grado di fare ciò che hai richiesto senza altre modifiche al tuo codice. Ecco un esempio:
Sprite tank = new Sprite(tankTexture);
tank.Children.Add(new Sprite(turretTexture) {Position = new Vector2(26, 16) });
spriteBatch.Begin();
tank.Draw(spriteBatch);
spriteBatch.End();
Implementazione
Per cominciare, inserirò semplicemente un progetto di esempio con questa tecnica implementata, nel caso in cui preferiate semplicemente guardare il codice e capirlo:
Nota: ho optato per la chiarezza anziché per le prestazioni qui. In un'implementazione seria, ci sono molte ottimizzazioni che potrebbero essere fatte, la maggior parte delle quali comporta la memorizzazione nella cache delle trasformazioni e il loro ricalcolo solo se necessario (ad esempio, memorizza nella cache sia le trasformazioni locali che globali per ogni sprite e ricalcola solo quando lo sprite o uno dei suoi antenati cambia). Inoltre, le versioni della matrice XNA e le operazioni vettoriali che prendono valori per riferimento sono un po 'più veloci di quelle che ho usato qui.
Ma descriverò il processo in modo più dettagliato di seguito, quindi continua a leggere per ulteriori informazioni.
Passaggio 1: apportare alcune modifiche alla classe Sprite
Supponendo che tu abbia già una Sprite
classe (e dovresti), allora dovrai apportare alcune modifiche. In particolare dovrai aggiungere l'elenco degli sprite figlio, la matrice di trasformazione locale e un modo per propagare trasforma la gerarchia degli sprite. Ho trovato il modo più semplice per farlo solo per passarli come parametro durante il disegno. Esempio:
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);
}
Passaggio 2: calcolo della matrice LocalTransform
La LocalTransform
matrice è solo una normale matrice mondiale costruita dalla posizione dello sprite, dalla rotazione e dai valori di scala. Per l'origine ho assunto il centro dello 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);
}
}
Passaggio 3: sapere come passare una matrice a SpriteBatch
Un problema con la SpriteBatch
classe è che il suo Draw
metodo non sa come prendere direttamente una matrice mondiale. Ecco un metodo di supporto per colmare questo 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);
}
Passaggio 4: rendering dello Sprite
Nota: il Draw
metodo accetta la trasformazione globale del genitore come parametro. Esistono altri modi per propagare queste informazioni, ma ho trovato questo facile da usare.
- Calcola la trasformazione globale moltiplicando la trasformazione locale per la trasformazione globale del genitore.
- Adatta la trasformazione globale
SpriteBatch
e rendi lo sprite corrente.
- Renderizza tutti i bambini passando loro l'attuale trasformazione globale come parametro.
Traducendolo in codice otterrai qualcosa del tipo:
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));
}
Quando si disegna lo sprite di root non c'è trasformazione genitore, quindi la si passa Matrix.Identity
. È possibile creare un sovraccarico per aiutare in questo caso:
public void Draw(SpriteBatch spriteBatch) { Draw(spriteBatch, Matrix.Identity); }