Konzept
Ich würde dieses Problem mit einer Sprite-Hierarchie unter Verwendung einer Variation des zusammengesetzten Entwurfsmusters lösen . Dies bedeutet, dass jedes Sprite eine Liste der untergeordneten Sprites speichert, die daran angehängt sind, sodass alle Änderungen am übergeordneten Sprite automatisch in ihnen berücksichtigt werden (einschließlich Übersetzung, Drehung und Skalierung).
In meinem Motor habe ich es so implementiert:
- Jeder
Sprite
speichert aList<Sprite> Children
und bietet eine Methode zum Hinzufügen neuer untergeordneter Elemente.
- Jeder
Sprite
weiß, wie man einen relativMatrix LocalTransform
definierten berechnet zum Elternteil definiert ist.
- Aufruf
Draw
an aSprite
auch alle seine Kinder angerufen.
- Kinder multiplizieren ihre lokale Transformation mit der globalen Transformation ihrer Eltern . Das Ergebnis verwenden Sie beim Rendern.
Auf diese Weise können Sie das tun, wonach Sie gefragt haben, ohne weitere Änderungen an Ihrem Code vornehmen zu müssen. Hier ist ein Beispiel:
Sprite tank = new Sprite(tankTexture);
tank.Children.Add(new Sprite(turretTexture) {Position = new Vector2(26, 16) });
spriteBatch.Begin();
tank.Draw(spriteBatch);
spriteBatch.End();
Implementierung
Für den Anfang werde ich einfach ein Beispielprojekt mit dieser implementierten Technik einfügen, falls Sie es vorziehen, nur den Code zu betrachten und es herauszufinden:
Hinweis: Ich habe mich hier für Klarheit statt Leistung entschieden. In einer seriösen Implementierung können viele Optimierungen vorgenommen werden, von denen die meisten das Zwischenspeichern von Transformationen und deren Neuberechnung nach Bedarf umfassen (z. B. lokale und globale Transformationen bei jedem Sprite zwischenspeichern und nur dann neu berechnen, wenn das Sprite oder einer seiner Vorfahren ändert sich). Außerdem sind die Versionen der Matrix- und Vektoroperationen von XNA, die Werte als Referenz verwenden, etwas schneller als die hier verwendeten.
Aber ich werde den Prozess weiter unten genauer beschreiben. Lesen Sie also weiter, um weitere Informationen zu erhalten.
Schritt 1 - Nehmen Sie einige Anpassungen an der Sprite-Klasse vor
Angenommen, Sie haben bereits eine Sprite
Klasse eingerichtet (und sollten dies auch tun), müssen Sie einige Änderungen daran vornehmen. Insbesondere müssen Sie die Liste der untergeordneten Sprites, die lokale Transformationsmatrix und eine Möglichkeit zum Weitergeben von Transformationen in der Sprite-Hierarchie hinzufügen. Ich fand es am einfachsten, dies nur zu tun, um sie beim Zeichnen als Parameter zu übergeben. Beispiel:
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);
}
Schritt 2 - Berechnung der LocalTransform-Matrix
Die LocalTransform
Matrix ist nur eine reguläre Weltmatrix, die aus den Positions-, Rotations- und Skalierungswerten des Sprites erstellt wird. Für den Ursprung habe ich das Zentrum des Sprites angenommen:
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);
}
}
Schritt 3 - Wissen, wie eine Matrix an SpriteBatch übergeben wird
Ein Problem mit der SpriteBatch
Klasse ist, dass ihre Draw
Methode nicht weiß, wie man eine Weltmatrix direkt nimmt. Hier ist eine Hilfsmethode, um dieses Problem zu beheben:
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);
}
Schritt 4 - Rendern des Sprites
Hinweis: Die Draw
Methode verwendet die globale Transformation des übergeordneten Elements als Parameter. Es gibt andere Möglichkeiten, diese Informationen zu verbreiten, aber ich fand diese einfach zu verwenden.
- Berechnen Sie die globale Transformation, indem Sie die lokale Transformation mit der globalen Transformation des übergeordneten Elements multiplizieren.
- Passen Sie die globale Transformation an
SpriteBatch
das aktuelle Sprite an und rendern Sie es.
- Rendern Sie allen untergeordneten Elementen die aktuelle globale Transformation als Parameter.
Wenn Sie das in Code übersetzen, erhalten Sie Folgendes:
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));
}
Beim Zeichnen des Root-Sprites gibt es keine übergeordnete Transformation. Sie übergeben sie also Matrix.Identity
. Sie können eine Überladung erstellen, um in diesem Fall zu helfen:
public void Draw(SpriteBatch spriteBatch) { Draw(spriteBatch, Matrix.Identity); }