Làm thế nào tôi có thể thực hiện hiệu quả một bitmask lớn hơn 64 bit để kiểm tra sự tồn tại của thành phần?


8

Trong triển khai ECS của tôi, tôi sử dụng các thao tác bit-khôn ngoan (như được mô tả và minh họa trong luồng này ) để cho một thực thể biết loại thành phần nào hiện tại bao gồm. Vì vậy, Entitylớp học của tôi có thành viên sau đây:

unsigned long m_CompMask;

Các loại thành phần khác nhau sau đó được định nghĩa là một enum, có giá trị thành viên là tất cả sức mạnh của hai:

enum ComponentType
{
    COMP_TYPE_UNKNOWN                   = 0,
    COMP_TYPE_PERSON                    = 1,
    COMP_TYPE_RENDERABLE                = 2,
    COMP_TYPE_MOVEABLE                  = 4,
    [...]
};

Mỗi lần một thành phần mới được thêm vào thể hiện thực thể của tôi, tôi thực hiện thao tác bit sau để cập nhật mặt nạ (trong đó newType là thành viên của các enumđề cập ở trên):

m_CompMask |= newType;

Cách tiếp cận này cho phép tôi kiểm tra hiệu quả nếu một thực thể nhất định có một thành phần nhất định (và do đó có thể phù hợp với một hệ thống cụ thể), như vậy:

return (m_CompMask & type);

Tuy nhiên, yếu tố giới hạn trong cách tiếp cận này là m_CompMaskbiến vì nó sẽ không thể xử lý hơn 64 loại thành phần (nếu tôi tăng nó lên unsigned long long).

Tôi không hy vọng sẽ cần nhiều hơn thế trong dự án hiện tại của mình, nhưng dù sao tôi cũng muốn nghe một số ý tưởng về các lựa chọn thay thế cho phép hơn 64 loại (như các trò chơi lớn chắc chắn cần) trong khi vẫn duy trì sự dễ dàng và hiệu quả của những hoạt động khôn ngoan nhất có thể? Bất kỳ ý tưởng làm thế nào điều này được xử lý trong các dự án "lớn thích hợp" ?


Tôi sẽ ngạc nhiên nếu ngay cả các trò chơi lớn cũng sử dụng hơn 64 loại thành phần khác nhau. Đây cũng có thể là ngôn ngữ cụ thể, nhưng tôi đoán nó sẽ phụ thuộc vào chiến lược.
MichaelHouse

Sử dụng hai độ dài 64 bit và bitwise VÀ hai lần :)!

2
Vui lòng viết trở lại m_CompMask & gõ; :-)
bogglez

@bogglez Điều đó thực sự thậm chí còn tốt hơn. Chỉnh sửa và thay đổi trong dự án của tôi. Cảm ơn!
Philip Allgaier

1
@ Byte56 Bộ bài trình chiếu này từ "Dungeon Siege" (2002: Windows, OS X) nói về hơn 150 thành phần ( gamedevs.org/uploads/data-driven-game-object-system.pdf ) và bộ bài trình chiếu này từ bộ bài này trò chơi "Nguyên mẫu" (2009: 360, PS3, Windows) ( gdcvault.com/play/1911/Theory-and-Practice-of-the )
Philip Allgaier

Câu trả lời:


7

Để thêm vào các câu trả lời khác ở đây, bạn có thể sử dụng một cái gì đó như trong đoạn mã sau để có một tập hợp độ dài tùy ý phát triển theo yêu cầu. Phương pháp này thậm chí cho phép bạn thực hiện một hình thức nén đơn giản; nếu các thành phần phổ biến nhất của bạn có giá trị ID thấp nhất, phần lớn các thực thể của bạn sẽ sử dụng một lượng nhỏ bộ nhớ bit để theo dõi các thành phần của chúng, nhưng các thành phần sử dụng các thành phần hiếm hơn vẫn sẽ hoạt động bình thường.

/// <summary>
/// A resizable collection of bits.
/// </summary>
public class BitSet {
    const int BitSize = (sizeof(uint) * 8) - 1;
    const int ByteSize = 5;  // log_2(BitSize + 1)

    uint[] bits;

    /// <summary>
    /// Initializes a new instance of the <see cref="BitSet"/> class.
    /// </summary>
    public BitSet () {
        bits = new uint[1];
    }

    /// <summary>
    /// Determines whether the given bit is set.
    /// </summary>
    /// <param name="index">The index of the bit to check.</param>
    /// <returns><c>true</c> if the bit is set; otherwise, <c>false</c>.</returns>
    public bool IsSet (int index) {
        int b = index >> ByteSize;
        if (b >= bits.Length)
            return false;

        return (bits[b] & (1 << (index & BitSize))) != 0;
    }

    /// <summary>
    /// Sets the bit at the given index.
    /// </summary>
    /// <param name="index">The bit to set.</param>
    public void SetBit (int index) {
        int b = index >> ByteSize;
        if (b >= bits.Length)
            Array.Resize(ref bits, b + 1);

        bits[b] |= 1u << (index & BitSize);
    }

    /// <summary>
    /// Clears the bit at the given index.
    /// </summary>
    /// <param name="index">The bit to clear.</param>
    public void ClearBit (int index) {
        int b = index >> ByteSize;
        if (b >= bits.Length)
            return;

        bits[b] &= ~(1u << (index & BitSize));
    }

    /// <summary>
    /// Sets all bits.
    /// </summary>
    public void SetAll () {
        int count = bits.Length;
        for (int i = 0; i < count; i++)
            bits[i] = 0xffffffff;
    }

    /// <summary>
    /// Clears all bits.
    /// </summary>
    public void ClearAll () {
        Array.Clear(bits, 0, bits.Length);
    }

    /// <summary>
    /// Determines whether all of the bits in this instance are also set in the given bitset.
    /// </summary>
    /// <param name="other">The bitset to check.</param>
    /// <returns><c>true</c> if all of the bits in this instance are set in <paramref name="other"/>; otherwise, <c>false</c>.</returns>
    public bool IsSubsetOf (BitSet other) {
        if (other == null)
            throw new ArgumentNullException("other");

        var otherBits = other.bits;
        int count = Math.Min(bits.Length, otherBits.Length);
        for (int i = 0; i < count; i++) {
            uint bit = bits[i];
            if ((bit & otherBits[i]) != bit)
                return false;
        }

        // handle extra bits on our side that might just be all zero
        int extra = bits.Length - count;
        for (int i = count; i < extra; i++) {
            if (bits[i] != 0)
                return false;
        }

        return true;
    }
}

Có khả năng hoàn toàn mạnh mẽ, bạn muốn triển khai một số giao diện khung như IEquitable và IComparable và ghi đè một vài toán tử, GetHashCode, v.v.


Tôi thích điều này hơn câu trả lời của tôi.
MichaelHouse

1
Tôi bối rối. Tất cả chúng ta đều đang viết C # ... vậy tại sao chúng ta không sử dụng thứ gì đó như BitSet hoặc BitArray?
Vaughan Hilts

1
@VaughanHilts C # có lớp BitArray và cấu trúc BitVector32. Cái trước không thể thay đổi kích thước sau khi khởi tạo; cái sau được cố định ở 32 bit và rất vô dụng cho tình huống này.
MikeP

3

Có lẽ bạn có thể sử dụng bitfield kiểu C và chỉ cần thêm các trường mới khi cần.

struct ComponentIds {   
    bool person     : 1;
    bool renderable : 1; 
    bool movable    : 1;
    bool ai         : 1;
    // and so on...
};

class GameObject {
public:

    ComponentIds componentsUsed;
};

Vì bạn chỉ cần cờ boolean, một struct bitfield sẽ nhỏ hơn rất nhiều so với đơn giản bools và đưa ra kết quả tương tự và dễ dàng sử dụng:

GameObject * go = ...
if (go->componentsUsed.ai)
{
    // has AI component
}

1

Một ví dụ đơn giản về cấu trúc dữ liệu lấp đầy mục đích này:

class LongerBitMask {

    //mask1_mask2

    long mask1
    long mask2

    public LongerBitMask() {
        mask1 = 0
        mask2 = 0
    }

    public LongerBitMask(long m1, long m2) {
        mask1 = m1
        mask2 = m2
    }

    public void OR(LongerBitMask other) {
        this.mask1 |= other.mask1
        this.mask2 |= other.mask2
    }

    public bool Match(LongerBitMask other) {
        return (this.mask1 & other.mask1) && (this.mask2 && other.mask2)
    }
}

Bây giờ, enum của bạn có thể lập chỉ mục vào một từ điển, hoặc một số loại. Tôi cho rằng khó hơn một chút để quản lý, nhưng tôi chưa nghĩ nhiều về kết thúc này.

Có lẽ chỉ cần có một mảng LongerBitMask đó được tạo ra tự động? Sau đó lập chỉ mục vào nó bằng enum? Chỉ cần đảm bảo chỉ thêm vào cuối enum. Nếu không, bạn sẽ có các thành phần thay đổi giá trị.

Một số mã tĩnh:

void CreateComponentBitMasks()
{
    LongerBitMasks[] bitMasks
    int index = 0

    bitMasks[index++] = new LongerBitMask(0, 0)

    for(int m2 = 0; m2 < 63; m2++) {
        bitMasks[index++] = new LongerBitMask(0, 1<<m2)
    }

    for(int m1 = 0; m1 < 63; m1++) {
        bitMasks[index++] = new LongerBitMask(1<<m1, 0)
    }
}

Đặt các thành phần của bạn thành giá trị enum mặc định của chúng, từ 0 đến 127 (không biến chúng thành sức mạnh của hai). Sau đó, bạn có thể truy xuất mặt nạ bit của mình với:

bitMasks[COMP_TYPE_RENDERABLE]; //would return bit mask with values 0, 2

Bây giờ hệ thống của bạn sẽ xác định LongBitMaskgiá trị của riêng họ và so sánh với các thực thể bằng cách sử dụngMatch .

Giống như một hệ thống sẽ tạo mặt nạ bit của riêng mình với:

//creates a bit mask to match components that are a renderable person.
LongerBitMask thisSystemMask = new LongerBitMask()
thisSystemMask.OR(bitMasks[COMP_TYPE_RENDERABLE])
thisSystemMask.OR(bitMasks[COMP_TYPE_PERSON])

Có khả năng có điều gì đó không ổn trong việc dịch chuyển bit của tôi hoặc toán học khác ...


0

Thật sự dễ dàng có thể sử dụng hơn 64 thành phần ngay cả trong một trò chơi phạm vi vừa phải.

Tạo lớp bitet riêng của bạn có thể giữ mặt nạ có độ dài tùy ý.


3
Một ví dụ sẽ làm cho điều này một câu trả lời tuyệt vời. (và có thể một số bình luận về tác động hiệu suất).
MichaelHouse

Cái mà chúng tôi sử dụng không phải do tôi tạo nên tôi không thể đăng mã. Nó cũng không phải là nhỏ, hơn 200 dòng mã.
karmington

Bất kỳ ví dụ sẽ làm.
MichaelHouse

"m_activeComponentMask.setBit (typeId);" Có một trình khởi tạo, các hàm như setBit và isBitset và quá tải toán tử cho tất cả các toán tử nhị phân. Lớp học không được nội tuyến nên tôi cho rằng chi phí không đáng kể.
karmington

Dungeon Siege có 169 (21 thành phần C ++, 148 tập lệnh) PDF
elFarto

0

Tôi không biết làm thế nào điều này được thực hiện trong các trò chơi AAA, nhưng vấn đề của bạn có vẻ khá đơn giản đối với tôi. Bạn chỉ cần hợp nhất với nhau:

  • Vùng nhớ giữ đủ bit.
  • Cách để thao tác các bit đã cho bằng chỉ mục.

Đây là mã ví dụ của tôi, trong C ++ 11. Tôi hy vọng bạn có thể dịch nó thành bất cứ điều gì bạn đang sử dụng, nhưng nếu bạn không thể, hãy hỏi tôi. Tôi nghĩ rằng tôi đã thấy một cái gì đó tương tự trong boost, nhưng không thể nhớ ngay bây giờ.

#include <iostream>

struct BitProxy
{
    char &ByteRef;
    char Mask;

    BitProxy& operator=(bool v)
    {
        if(v) {ByteRef |= Mask;}
        else {ByteRef &= ~Mask;}
        return *this;
    }

    operator bool() const {return ByteRef & Mask;}
};

template <unsigned NumBits, typename EnumType, typename UnderlyingType>
struct EnumSet
{
    // how much bytes we need
    static constexpr unsigned Size = (NumBits - 1) / 8 + 1;
    char Bytes[Size];

    BitProxy operator[](EnumType v)
    {
        unsigned byte_ix = unsigned(UnderlyingType(v)) / 8;
        unsigned bit_ix = unsigned(UnderlyingType(v)) & 7;
        return BitProxy {Bytes[byte_ix], char(1 << bit_ix)};
    }
};

namespace Component
{
    enum class Id : unsigned
    {
        One,
        Two,
        Three,
        c4, c5, c6, c7, c8, c9, c10,
        // this special one will tells us how much bits we need
        Last_
    };

    static constexpr Id AllIds[] =
    {
        // well...
        Id::One, Id::Two, Id::Three, Id::c4, Id::c5, Id::c6, Id::c7, Id::c8, Id::c9, Id::c10
    };

    typedef EnumSet<unsigned(Id::Last_), Id, unsigned> Set;
}

int main()
{
    Component::Set cs {};
    cs[Component::Id::Two] = true;
    cs[Component::Id::c7] = true;
    for(Component::Id id : Component::AllIds)
        std::cout << unsigned(id) << " -> " << (cs[id] ? "true":"false") << std::endl;
}

http://coliru.stacked-crooking.com/a/63904a428e95df94 Kết quả:

0 -> false
1 -> true
2 -> false
3 -> false
4 -> false
5 -> false
6 -> true
7 -> false
8 -> false
9 -> false

Được sử dụng struct insted của class để đơn giản, nhưng tôi khuyên nên đóng gói nó đúng cách.

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.