концепция
Я бы решил эту проблему с помощью иерархии спрайтов, используя вариацию шаблона составного проекта . Это означает, что каждый спрайт хранит список дочерних спрайтов, которые прикреплены к нему, так что любые модификации родительского объекта автоматически отражаются в них (включая перевод, вращение и масштабирование).
В моем движке я реализовал это так:
- Каждый
Sprite
хранит List<Sprite> Children
и предоставляет метод для добавления новых дочерних элементов.
- Каждый
Sprite
знает, как рассчитать, Matrix LocalTransform
который определен относительно родителя.
- Вызов
Draw
на Sprite
также вызывает его на всех своих детей.
- Дети умножают свои локальные преобразования на глобальные преобразования своих родителей . Результатом является то, что вы используете при рендеринге.
Благодаря этому вы сможете делать то, что просили, без каких-либо других изменений в вашем коде. Вот пример:
Sprite tank = new Sprite(tankTexture);
tank.Children.Add(new Sprite(turretTexture) {Position = new Vector2(26, 16) });
spriteBatch.Begin();
tank.Draw(spriteBatch);
spriteBatch.End();
Реализация
Для начала я просто добавлю пример проекта с этой техникой, если вы предпочитаете просто посмотреть на код и разобраться с ним:
Примечание: я выбрал ясность вместо производительности здесь. В серьезной реализации можно выполнить множество оптимизаций, большинство из которых включают кэширование преобразований и их пересчет только по мере необходимости (например, кэширование локальных и глобальных преобразований для каждого спрайта и их пересчет только тогда, когда спрайт или один из его предков меняется). Кроме того, версии матричных и векторных операций XNA, которые принимают значения по ссылке, работают немного быстрее, чем те, которые я использовал здесь.
Но я опишу процесс более подробно ниже, так что читайте дальше для получения дополнительной информации.
Шаг 1 - внесите несколько коррективов в класс Sprite
Предполагая, что у вас уже есть Sprite
класс (и вы должны это сделать), вам нужно будет внести в него несколько изменений. В частности, вам нужно добавить список дочерних спрайтов, матрицу локальных преобразований и способ распространения преобразований по иерархии спрайтов. Я нашел самый простой способ сделать это просто передать их в качестве параметра при рисовании. Пример:
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);
}
Шаг 2 - Расчет матрицы LocalTransform
LocalTransform
Матрица просто обычный мир матрица строится из значений положения, поворота и масштаба спрайта. В качестве источника я взял центр спрайта:
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);
}
}
Шаг 3 - Знание того, как передать Матрицу в SpriteBatch
Одна проблема с SpriteBatch
классом состоит в том, что его Draw
метод не знает, как получить мировую матрицу напрямую. Вот вспомогательный метод для решения этой проблемы:
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);
}
Шаг 4 - Рендеринг Спрайта
Примечание. Draw
Метод принимает глобальное преобразование родителя в качестве параметра. Есть и другие способы распространения этой информации, но я обнаружил, что этот способ прост в использовании.
- Вычислить глобальное преобразование, умножив локальное преобразование на глобальное преобразование родителя.
- Адаптируйте глобальное преобразование
SpriteBatch
и отобразите текущий спрайт.
- Визуализируйте все дочерние элементы, передав им текущее глобальное преобразование в качестве параметра.
Преобразовав это в код, вы получите что-то вроде:
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));
}
При рисовании корневого спрайта родительское преобразование отсутствует, поэтому вы передаете его Matrix.Identity
. Вы можете создать перегрузку, чтобы помочь в этом случае:
public void Draw(SpriteBatch spriteBatch) { Draw(spriteBatch, Matrix.Identity); }