// MIT License - Copyright (c) 2025 wallstop // Full license text: https://github.com/wallstop/unity-helpers/blob/main/LICENSE namespace WallstopStudios.UnityHelpers.Core.DataStructure { using System; using System.Collections; using System.Collections.Generic; using System.Runtime.CompilerServices; using ProtoBuf; using UnityEngine; using WallstopStudios.UnityHelpers.Utils; /// /// Compact, dynamically resizable bit set that stores dense boolean flags using a single bit per entry. /// Ideal for entity state masks, collision layers, and other scenarios that benefit from memory-efficient O(1) reads and writes. /// /// /// /// [Serializable] [ProtoContract(IgnoreListHandling = true)] public sealed class BitSet : IReadOnlyList { private const int BitsPerLong = 64; private const int BitsPerLongShift = 6; // log2(64) private const int BitsPerLongMask = 63; // 64 - 1 private const int DefaultCapacity = 64; [SerializeField] [ProtoMember(1)] private ulong[] _bits; [SerializeField] [ProtoMember(2)] private int _capacity; public struct BitEnumerator : IEnumerator { private readonly BitSet _bitSet; private int _index; private bool _current; internal BitEnumerator(BitSet bitSet) { _bitSet = bitSet; _index = -1; _current = false; } public bool MoveNext() { if (++_index < _bitSet._capacity) { _bitSet.TryGet(_index, out _current); return true; } _current = false; return false; } public bool Current => _current; object IEnumerator.Current => Current; public void Reset() { _index = -1; _current = false; } public void Dispose() { } } public int Count => _capacity; /// /// Gets the current capacity (maximum number of bits that can be stored without resizing). /// public int Capacity => _capacity; /// /// Gets or sets the bit at the specified index. /// Automatically expands if setting a bit beyond current capacity. /// public bool this[int index] { get => TryGet(index, out bool value) && value; set { if (value) { TrySet(index); } else { TryClear(index); } } } private BitSet() { _capacity = 0; _bits = Array.Empty(); } /// /// Constructs a bit set with the specified initial capacity. /// public BitSet(int initialCapacity) { if (initialCapacity <= 0) { throw new ArgumentOutOfRangeException( nameof(initialCapacity), "Initial capacity must be positive." ); } _capacity = initialCapacity; int arraySize = (initialCapacity + BitsPerLong - 1) >> BitsPerLongShift; _bits = new ulong[arraySize]; } /// /// Attempts to set the bit at the specified index to 1. /// Automatically expands capacity if needed. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool TrySet(int index) { if (index < 0) { return false; } if (index >= _capacity) { EnsureCapacity(index + 1); } int arrayIndex = index >> BitsPerLongShift; int bitIndex = index & BitsPerLongMask; _bits[arrayIndex] |= 1UL << bitIndex; return true; } /// /// Attempts to clear the bit at the specified index to 0. /// Returns false if index is negative or beyond current capacity (does not auto-expand). /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool TryClear(int index) { if (index < 0 || index >= _capacity) { return false; } int arrayIndex = index >> BitsPerLongShift; int bitIndex = index & BitsPerLongMask; _bits[arrayIndex] &= ~(1UL << bitIndex); return true; } /// /// Attempts to toggle the bit at the specified index. /// Automatically expands capacity if needed. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool TryFlip(int index) { if (index < 0) { return false; } if (index >= _capacity) { EnsureCapacity(index + 1); } int arrayIndex = index >> BitsPerLongShift; int bitIndex = index & BitsPerLongMask; _bits[arrayIndex] ^= 1UL << bitIndex; return true; } /// /// Attempts to get the bit at the specified index. /// Returns false (out parameter) for indices beyond capacity. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool TryGet(int index, out bool value) { if (index < 0 || index >= _capacity) { value = false; return false; } int arrayIndex = index >> BitsPerLongShift; int bitIndex = index & BitsPerLongMask; value = (_bits[arrayIndex] & (1UL << bitIndex)) != 0; return true; } /// /// Ensures the bit set can hold at least the specified capacity. /// public void EnsureCapacity(int minCapacity) { if (minCapacity <= _capacity) { return; } int newCapacity = _capacity; while (newCapacity < minCapacity) { newCapacity = newCapacity < 256 ? newCapacity * 2 : newCapacity + (newCapacity / 2); } Resize(newCapacity); } /// /// Resizes the bit set to the specified capacity. /// If shrinking, bits beyond the new capacity are lost. /// public void Resize(int newCapacity) { if (newCapacity <= 0) { throw new ArgumentOutOfRangeException( nameof(newCapacity), "Capacity must be positive." ); } if (newCapacity == _capacity) { return; } int newArraySize = (newCapacity + BitsPerLong - 1) >> BitsPerLongShift; Array.Resize(ref _bits, newArraySize); _capacity = newCapacity; // Clear any bits beyond new capacity in the last element int remainingBits = newCapacity & BitsPerLongMask; if (remainingBits != 0 && newArraySize > 0) { ulong mask = (1UL << remainingBits) - 1; _bits[newArraySize - 1] &= mask; } } /// /// Shrinks the capacity to fit the highest set bit, or to a minimum capacity. /// public void TrimExcess(int minimumCapacity = DefaultCapacity) { int highestSetBit = -1; for (int i = _capacity - 1; i >= 0; i--) { if (TryGet(i, out bool value) && value) { highestSetBit = i; break; } } int targetCapacity = Math.Max(minimumCapacity, highestSetBit + 1); if (targetCapacity < _capacity) { Resize(targetCapacity); } } /// /// Sets all bits to 1 within the current capacity. /// public void SetAll() { for (int i = 0; i < _bits.Length; i++) { _bits[i] = ulong.MaxValue; } // Clear any bits beyond capacity in the last element int remainingBits = _capacity & BitsPerLongMask; if (remainingBits != 0 && _bits.Length > 0) { ulong mask = (1UL << remainingBits) - 1; _bits[^1] &= mask; } } /// /// Sets all bits to 0. /// public void ClearAll() { Array.Clear(_bits, 0, _bits.Length); } /// /// Flips all bits (0 becomes 1, 1 becomes 0) within the current capacity. /// public void FlipAll() { for (int i = 0; i < _bits.Length; i++) { _bits[i] = ~_bits[i]; } // Clear any bits beyond capacity in the last element int remainingBits = _capacity & BitsPerLongMask; if (remainingBits != 0 && _bits.Length > 0) { ulong mask = (1UL << remainingBits) - 1; _bits[^1] &= mask; } } /// /// Inverts all bits (same as FlipAll). /// public void Not() { FlipAll(); } /// /// Shifts all bits left by the specified amount. /// Bits shifted beyond capacity are lost, zeros fill from the right. /// public void LeftShift(int shift) { if (shift <= 0) { return; } if (shift >= _capacity) { ClearAll(); return; } // Rent a temporary array from the pool to avoid reading already-modified values using PooledArray pooled = SystemArrayPool.Get( _bits.Length, out ulong[] temp ); Array.Copy(_bits, temp, _bits.Length); // Clear all bits first ClearAll(); // Shift bit by bit from the temporary copy for (int i = shift; i < _capacity; i++) { int sourceIndex = i - shift; int sourceWordIndex = sourceIndex >> 6; int sourceBitIndex = sourceIndex & 63; bool value = (temp[sourceWordIndex] & (1UL << sourceBitIndex)) != 0; if (value) { TrySet(i); } } } /// /// Shifts all bits right by the specified amount. /// Bits shifted out are lost, zeros fill from the left. /// public void RightShift(int shift) { if (shift <= 0) { return; } if (shift >= _capacity) { ClearAll(); return; } // Shift bit by bit for correctness for (int i = 0; i < _capacity - shift; i++) { TryGet(i + shift, out bool value); if (value) { TrySet(i); } else { TryClear(i); } } // Clear the upper bits for (int i = _capacity - shift; i < _capacity; i++) { TryClear(i); } } /// /// Counts the number of bits set to 1. /// public int CountSetBits() { int count = 0; foreach (ulong bit in _bits) { count += PopCount(bit); } return count; } /// /// Checks if any bits are set to 1. /// public bool Any() { return Array.Exists(_bits, bit => bit != 0); } /// /// Checks if all bits are set to 0. /// public bool None() { return !Any(); } /// /// Checks if all bits (within capacity) are set to 1. /// public bool All() { if (_capacity <= 0) { return false; } int fullSegments = _capacity >> BitsPerLongShift; for (int i = 0; i < fullSegments; i++) { if (_bits[i] != ulong.MaxValue) { return false; } } int remainingBits = _capacity & BitsPerLongMask; if (remainingBits == 0) { return true; } if (_bits.Length <= fullSegments) { return false; } ulong mask = (1UL << remainingBits) - 1; return (_bits[fullSegments] & mask) == mask; } /// /// Performs a bitwise AND operation with another BitSet. /// Resizes this BitSet to match the other if needed. /// public bool TryAnd(BitSet other) { if (other == null) { return false; } if (other._capacity != _capacity) { Resize(other._capacity); } for (int i = 0; i < _bits.Length; i++) { _bits[i] &= other._bits[i]; } return true; } /// /// Performs a bitwise OR operation with another BitSet. /// Resizes this BitSet to match the other if needed. /// public bool TryOr(BitSet other) { if (other == null) { return false; } if (other._capacity > _capacity) { Resize(other._capacity); } int minLength = Math.Min(_bits.Length, other._bits.Length); for (int i = 0; i < minLength; i++) { _bits[i] |= other._bits[i]; } return true; } /// /// Performs a bitwise XOR operation with another BitSet. /// Resizes this BitSet to match the other if needed. /// public bool TryXor(BitSet other) { if (other == null) { return false; } if (other._capacity > _capacity) { Resize(other._capacity); } int minLength = Math.Min(_bits.Length, other._bits.Length); for (int i = 0; i < minLength; i++) { _bits[i] ^= other._bits[i]; } return true; } /// /// Populates the provided list with indices of all set bits. /// Clears the list before adding. Returns the same list for chaining. /// public List GetSetBits(List results) { if (results == null) { throw new ArgumentNullException(nameof(results)); } results.Clear(); for (int i = 0; i < _capacity; i++) { if (TryGet(i, out bool value) && value) { results.Add(i); } } return results; } /// /// Enumerates indices of all set bits (where value is 1). /// Use this for efficient iteration over sparse bitsets. /// public IEnumerable EnumerateSetIndices() { for (int i = 0; i < _capacity; i++) { if (TryGet(i, out bool value) && value) { yield return i; } } } /// /// Converts this BitSet to an immutable ImmutableBitSet. /// Creates a snapshot with a copy of the current bit data. /// public ImmutableBitSet ToImmutable() { // Create a copy of the bits array to ensure immutability ulong[] bitsCopy = new ulong[_bits.Length]; Array.Copy(_bits, bitsCopy, _bits.Length); return new ImmutableBitSet(bitsCopy, _capacity); } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static int PopCount(ulong value) { // Brian Kernighan's algorithm int count = 0; while (value != 0) { value &= value - 1; count++; } return count; } /// /// Returns an enumerator that iterates through all bit values (true/false) in order. /// Use foreach to iterate over all bits. /// public BitEnumerator GetEnumerator() { return new BitEnumerator(this); } IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } } }