Có một cách đơn giản để nhóm hai hoặc nhiều sprite, vì vậy tất cả chúng sẽ phụ thuộc vào nhau?


8

Tôi nghĩ rằng câu hỏi này rất giống với câu hỏi này , nhưng tôi không chắc câu trả lời có phổ biến không.

Vì vậy, mục tiêu của tôi là:

  • Đặt hai sprite ở vị trí cố định, ví dụ như người chơi và đôi mắt của anh ấy
  • Hãy chắc chắn rằng bất cứ khi nào người chơi xoay, mắt cũng xoay theo và đến cùng một vị trí tương đối từ cơ thể (để mắt không nằm trên lưng người chơi). Vì vậy, họ sẽ làm việc như một nhóm. - Bước này nên được tự động hóa, đó là mục tiêu của tôi!

Vì vậy, ví dụ, bây giờ tôi muốn đặt một khẩu súng trong tay người dùng. Vì vậy, bây giờ tôi nói, người chơi đó ở vị trí Vector2(0, 0)và súng ở vị trí Vector2(26, 16). Bây giờ tôi muốn nhóm chúng lại, vì vậy bất cứ khi nào người chơi xoay, súng cũng xoay theo.

nhập mô tả hình ảnh ở đây

Hiện tại trong ví dụ này khá ổn nhưng trong trường hợp tôi cần di chuyển khẩu súng của mình trên trục y (chỉ), tôi đã bị mất

Câu trả lời:


10

Ý tưởng

Tôi sẽ giải quyết vấn đề này với hệ thống phân cấp sprite bằng cách sử dụng một biến thể của mẫu thiết kế tổng hợp . Điều này có nghĩa là mỗi sprite lưu trữ một danh sách các sprite con được gắn vào nó để mọi sửa đổi cho cha mẹ được tự động phản ánh trong chúng (bao gồm dịch, xoay và chia tỷ lệ).

Trong công cụ của tôi, tôi đã thực hiện nó như thế này:

  • Mỗi Spritecửa hàng một List<Sprite> Childrenvà cung cấp một phương pháp để thêm trẻ em mới.
  • Mỗi người Spritebiết cách tính một Matrix LocalTransformcái được định nghĩa liên quan đến cha mẹ.
  • Gọi Drawcho một Spritecũng gọi nó trên tất cả các con của nó.
  • Trẻ em nhân lên sự biến đổi cục bộ của chúng bằng sự biến đổi toàn cầu của cha mẹ chúng . Kết quả là những gì bạn sử dụng khi kết xuất.

Với điều này, bạn sẽ có thể làm những gì bạn yêu cầu mà không cần sửa đổi gì khác về mã của bạn. Đây là một ví dụ:

Sprite tank = new Sprite(tankTexture);
tank.Children.Add(new Sprite(turretTexture) {Position = new Vector2(26, 16) });

spriteBatch.Begin();
tank.Draw(spriteBatch);
spriteBatch.End();

Thực hiện

Để bắt đầu, tôi sẽ chỉ tham gia vào một dự án mẫu với kỹ thuật này được triển khai, trong trường hợp bạn chỉ muốn nhìn vào mã và tìm ra nó:

nhập mô tả hình ảnh ở đây

Lưu ý: Tôi đã chọn cho sự rõ ràng thay vì hiệu suất ở đây. Trong một triển khai nghiêm túc, có rất nhiều tối ưu hóa có thể được thực hiện, hầu hết trong số đó liên quan đến các biến đổi bộ đệm và chỉ tính toán lại khi cần thiết (ví dụ, lưu trữ cả biến đổi cục bộ và biến đổi toàn cục ở mỗi sprite và chỉ tính lại chúng khi sprite hoặc một trong những thay đổi tổ tiên của nó). Ngoài ra, các phiên bản hoạt động ma trận và vectơ của XNA lấy giá trị theo tham chiếu nhanh hơn một chút so với các phiên bản tôi đã sử dụng ở đây.

Nhưng tôi sẽ mô tả quá trình chi tiết hơn dưới đây, vì vậy hãy đọc tiếp để biết thêm thông tin.


Bước 1 - Thực hiện một vài điều chỉnh cho lớp Sprite

Giả sử bạn đã có một Spritelớp tại chỗ (và bạn nên) thì bạn sẽ cần phải thực hiện một vài sửa đổi cho nó. Cụ thể, bạn sẽ cần thêm danh sách các họa tiết con, ma trận biến đổi cục bộ và một cách để truyền bá các biến đổi xuống hệ thống phân cấp sprite. Tôi thấy cách dễ nhất để làm điều đó chỉ là vượt qua chúng như một tham số khi vẽ. Thí dụ:

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);
}

Bước 2 - Tính toán ma trận LocalTransform

Các LocalTransformma trận chỉ là một ma trận trên thế giới thường xuyên được xây dựng từ vị trí, luân chuyển và quy mô giá trị của sprite. Đối với nguồn gốc, tôi giả sử trung tâm của 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);
   }
}

Bước 3 - Biết cách chuyển Ma trận sang SpriteBatch

Một vấn đề với SpriteBatchlớp là Drawphương thức của nó không biết cách lấy ma trận thế giới trực tiếp. Đây là một phương pháp trợ giúp để khắc phục vấn đề này:

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);
}

Bước 4 - Kết xuất Sprite

Lưu ý: DrawPhương thức lấy biến đổi toàn cục của cha mẹ làm tham số. Có nhiều cách khác để tuyên truyền thông tin này, nhưng tôi thấy cách này dễ sử dụng.

  1. Tính toán biến đổi toàn cầu bằng cách nhân biến đổi cục bộ với biến đổi toàn cầu của cha mẹ.
  2. Điều chỉnh biến đổi toàn cục thành SpriteBatchvà hiển thị sprite hiện tại.
  3. Kết xuất tất cả trẻ em chuyển cho chúng biến đổi toàn cầu hiện tại làm tham số.

Dịch mã đó thành mã bạn sẽ nhận được một cái gì đó như:

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));
}

Khi vẽ sprite gốc không có biến đổi cha, vì vậy bạn vượt qua nó Matrix.Identity. Bạn có thể tạo quá tải để trợ giúp trong trường hợp này:

public void Draw(SpriteBatch spriteBatch) { Draw(spriteBatch, Matrix.Identity); }

Tôi có một vấn đề tôi hiện đang giải quyết. Trong thiết lập hiện tại của tôi, tôi tạo chuyển đổi Ma trận khi gọi spriteBatch, vì tôi đang sử dụng máy ảnh. Vì vậy, trong trường hợp máy ảnh của tôi ở mức 500,50 và sprite của người chơi của tôi trên 500,50, người chơi nên ở giữa. Tuy nhiên, trong trường hợp này, nó không vẽ ở đâu cả
Martin.

@Martin Cho dù bạn có sử dụng ma trận camera hay không thì nó cũng hoạt động như nhau. Trong ví dụ này tôi không sử dụng máy ảnh, nhưng trên động cơ của tôi, tôi sử dụng một máy ảnh và nó hoạt động bình thường. Bạn có đang truyền ma trận camera (và chỉ ma trận camera) cho SpriteBatch.Begin không? Và nếu bạn muốn mọi thứ được tập trung, bạn có tính đến một nửa kích thước màn hình khi tạo ma trận camera không?
David Gouveia

Tôi đang sử dụng cùng một lớp như ở đây cho máy ảnh, có được máy ảnh metrix, vâng. Bây giờ nó hoạt động, tôi sẽ cố gắng điều chỉnh nó ngay bây giờ và cho bạn biết
Martin.

1
HOÀN HẢO! HOÀN HẢO tuyệt vời! Và như trong hầu hết các trường hợp tôi gặp vấn đề, đó là lỗi của tôi. Hãy xem! i.imgur.com/2CDRP.png
Martin.

Tôi biết tôi đến bữa tiệc muộn ở đây vì câu hỏi này thực sự cũ. Nhưng trong khi điều này hoạt động, nó sẽ làm rối các vị trí khi tỷ lệ x và y không giống nhau: /
Erik Skoglund

2

Bạn sẽ có thể nhóm chúng thành một SpriteBatch và chúng di chuyển nó vào vị trí và xoay nó bằng ma trận.

var matrix = 
    Matrix.CreateRotationZ(radians) *
    Matrix.CreateTranslation(new Vector3(x, y, 0));

SpriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, null, null, null, null, matrix);
SpriteBatch.Draw(player, Vector2.Zero, null, Color.White);
SpriteBatch.Draw(gun, Vector2(handDistance, 0), null, Color.White);
SpriteBatch.End();

Các mã không được kiểm tra và phép nhân ma trận của tôi rất rỉ sét nhưng kỹ thuật này vẫn đúng.


Phải làm gì trong trường hợp tôi thực sự sử dụng Ma trận trong spriteBatch cho máy ảnh? spriteBatch.Begin(SpriteSortMode.Immediate, BlendState.AlphaBlend, null, null, null, null, cam.get_transformation(graphics.GraphicsDevice));
Martin.

Tôi nghĩ bạn sẽ có thể sử dụng Bắt đầu với ma trận * cam.get_transatures (Graphics.GraphicsDevice).
ClassicThunder

HOÀN HẢO. Tôi đã không có một câu trả lời tuyệt vời nhanh như vậy.
Martin.

Giải pháp đơn giản, nhưng nó đánh bại toàn bộ mục đích của việc tạo sprite, đó là vẽ càng nhiều sprite càng tốt trong một cuộc gọi. Tôi có thể sử dụng điều này trong một bản demo đơn giản, nhưng chắc chắn không phải trong một trò chơi chuyên sâu.
David Gouveia

@Martin Trong câu trả lời của tôi, tôi có một ví dụ về cách tạo ma trận thế giới hoàn chỉnh cho sprite. Về cơ bản thứ tự phải là -Origin * Scale * Rotation * Translation(trong đó tất cả các giá trị này đến từ sprite).
David Gouveia

0

Tôi sẽ thực hiện điều này một chút khác nhau.

Cung cấp cho lớp sprite của bạn một danh sách cho trẻ em của nó. Sau đó, khi bạn cập nhật vị trí và các biến đổi của sprite, cũng áp dụng các bản dịch tương tự cho trẻ em với một vòng lặp for-mỗi. Các họa tiết con nên có tọa độ được xác định trong không gian mô hình thay vì không gian thế giới.

Tôi thích sử dụng hai phép quay - một xoay quanh gốc của đứa trẻ (để quay sprite tại chỗ) và một xoay quanh gốc của cha mẹ (để làm cho đứa trẻ về cơ bản quay quanh cha mẹ).

Để vẽ, chỉ cần bắt đầu spritebatch của bạn, gọi player.draw () của bạn sẽ vẽ sau đó lặp qua tất cả các con của nó và vẽ chúng.

Với kiểu phân cấp này, khi bạn di chuyển cha mẹ, tất cả con cái của nó cũng di chuyển theo nó - cũng cho phép bạn di chuyển con cái một cách độc lập.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.