I recently ran into an interviewer who couldn’t understand why skills and buff systems use bitmasks. I tried explaining 2<<1=4 and 4<<1=8, brought up notification dots and Unity’s LayerMask as examples—still didn’t land. I lost my temper and then got told I was “hard to communicate with,” lol. Anyway, here’s a primer to jog memories and spark ideas on practical ways to use bitmasks—consider it both a refresher and an expansion.
Practical Bitwise Operations and Bitmasks in Unity
Bitwise basics
Core bitwise operators
using UnityEngine;
public class BitOperationBasics : MonoBehaviour
{
void Start()
{
// Basic bitwise operations
int a = 5; // binary: 0101
int b = 3; // binary: 0011
// Bitwise AND (&) - result bit is 1 only if both bits are 1
int and = a & b; // 0001 = 1
// Bitwise OR (|) - result bit is 1 if any bit is 1
int or = a | b; // 0111 = 7
// Bitwise XOR (^) - result bit is 1 if bits differ
int xor = a ^ b; // 0110 = 6
// Bitwise NOT (~) - flips bits: 0 -> 1, 1 -> 0
int not = ~a; // 11111010 = -6 (two's complement)
// Left shift (<<) - shift left, fill with 0 on the right
int leftShift = a << 2; // 010100 = 20
// Right shift (>>) - shift right, sign-extend on the left
int rightShift = a >> 1; // 0010 = 2
Debug.Log($"AND: {and}, OR: {or}, XOR: {xor}");
Debug.Log($"NOT: {not}, LEFT: {leftShift}, RIGHT: {rightShift}");
}
}
Handy bit utilities
public static class BitUtils
{
/// <summary>
/// Set bit i to 1.
/// Use cases: enable a feature, unlock content, set a status.
/// </summary>
public static uint SetBit(uint flags, int i)
{
return flags |= (1u << i);
}
/// <summary>
/// Clear bit i to 0.
/// Use cases: disable a feature, lock content, remove a status.
/// </summary>
public static uint ClearBit(uint flags, int i)
{
return flags &= ~(1u << i);
}
/// <summary>
/// Test if bit i is 1.
/// Use cases: status checks, permission checks, branching.
/// </summary>
public static bool TestBit(uint flags, int i)
{
return (flags & (1u << i)) != 0;
}
/// <summary>
/// Toggle bit i.
/// Use cases: on/off toggles, state inversion, UI interactions.
/// </summary>
public static uint ToggleBit(uint flags, int i)
{
return flags ^= (1u << i);
}
/// <summary>
/// Clear the lowest set bit (rightmost 1).
/// Use cases: process active flags one-by-one, priority queues, resource allocation.
/// </summary>
public static uint ClearLowestSetBit(uint x)
{
return x &= x - 1;
}
/// <summary>
/// Isolate the lowest set bit (rightmost 1).
/// Use cases: find the next item to process, bit scan, priority handling.
/// </summary>
public static uint IsolateLowestSetBit(uint x)
{
return x & (uint)-(int)x;
}
/// <summary>
/// Count number of set bits (population count).
/// Use cases: count actives, resource usage, degree of fulfillment.
/// </summary>
public static int CountSetBits(uint x)
{
int count = 0;
while (x != 0)
{
x = ClearLowestSetBit(x);
count++;
}
return count;
}
/// <summary>
/// Find index of the lowest set bit.
/// Use cases: find the first matching index.
/// </summary>
public static int FindLowestSetBitIndex(uint x)
{
if (x == 0) return -1;
int index = 0;
uint isolated = IsolateLowestSetBit(x);
while (isolated > 1)
{
isolated >>= 1;
index++;
}
return index;
}
}
What is a bitmask?
A bitmask uses bitwise operations to store and manipulate multiple boolean states within a single integer. Packing dozens of booleans into one integer can significantly reduce memory and improve performance.
// Traditional approach - uses many bytes
public class TraditionalFlags
{
public bool canJump;
public bool canFly;
public bool canSwim;
public bool canClimb;
public bool isInvincible;
public bool isVisible;
// ... more flags
}
// Bitmask approach - a single int (4 bytes)
[System.Flags]
public enum PlayerAbilities
{
None = 0,
CanJump = 1 << 0, // 0001
CanFly = 1 << 1, // 0010
CanSwim = 1 << 2, // 0100
CanClimb = 1 << 3, // 1000
IsInvincible = 1 << 4, // 10000
IsVisible = 1 << 5, // 100000
// Composites
BasicMovement = CanJump | CanSwim,
AllAbilities = CanJump | CanFly | CanSwim | CanClimb | IsInvincible | IsVisible
}
Common Unity use cases
LayerMask
LayerMask is a textbook bitmask in Unity, used to filter what layers are affected by raycasts, overlaps, collisions, etc.
using UnityEngine;
public class LayerMaskExample : MonoBehaviour
{
[SerializeField] private LayerMask groundLayers;
[SerializeField] private LayerMask enemyLayers;
[SerializeField] private LayerMask interactableLayers;
void Update()
{
// Raycast against specific layers
if (Physics.Raycast(transform.position, Vector3.down, out RaycastHit hit, 10f, groundLayers))
{
Debug.Log("Hit ground: " + hit.collider.name);
}
// Sphere overlap against multiple layers
Collider[] enemies = Physics.OverlapSphere(transform.position, 5f, enemyLayers);
foreach (var enemy in enemies)
{
Debug.Log("Found enemy: " + enemy.name);
}
}
void Start()
{
// Build a LayerMask manually
LayerMask customMask = 0;
customMask |= 1 << LayerMask.NameToLayer("Ground");
customMask |= 1 << LayerMask.NameToLayer("Platform");
// Check whether a layer is included
bool isGroundIncluded = (groundLayers & (1 << LayerMask.NameToLayer("Ground"))) != 0;
Debug.Log("Ground included: " + isGroundIncluded);
// Remove a layer from the mask
groundLayers &= ~(1 << LayerMask.NameToLayer("Water"));
// Add a layer to the mask
groundLayers |= 1 << LayerMask.NameToLayer("Ice");
}
}
Notification dots (red-dot badges)
Notification-dot systems are a very common UI pattern in games. Bitwise operations make it efficient to manage complex badge states.
using UnityEngine;
using System.Collections.Generic;
[System.Flags]
public enum RedDotType : uint
{
None = 0,
// Main menu (bits 0–7)
MainMenu = 1u << 0,
Store = 1u << 1,
Mail = 1u << 2,
Achievement = 1u << 3,
// Inventory (bits 8–15)
Inventory = 1u << 8,
Equipment = 1u << 9,
Consumables = 1u << 10,
// Quests (bits 16–23)
DailyQuest = 1u << 16,
MainQuest = 1u << 17,
SideQuest = 1u << 18,
// Social (bits 24–31)
Friends = 1u << 24,
Guild = 1u << 25,
Chat = 1u << 26,
// Grouped badges
AllQuests = DailyQuest | MainQuest | SideQuest,
AllInventory = Inventory | Equipment | Consumables,
AllSocial = Friends | Guild | Chat
}
public class RedDotManager : MonoBehaviour
{
private static RedDotManager instance;
public static RedDotManager Instance => instance;
private uint redDotFlags = 0;
private Dictionary<RedDotType, List<System.Action<bool>>> redDotCallbacks
= new Dictionary<RedDotType, List<System.Action<bool>>>();
void Awake()
{
if (instance == null)
{
instance = this;
DontDestroyOnLoad(gameObject);
}
else
{
Destroy(gameObject);
}
}
/// <summary>
/// Set a badge state.
/// </summary>
public void SetRedDot(RedDotType type, bool show)
{
bool wasSet = HasRedDot(type);
if (show)
{
redDotFlags = BitOperations.SetBit(redDotFlags, GetBitIndex(type));
}
else
{
redDotFlags = BitOperations.ClearBit(redDotFlags, GetBitIndex(type));
}
// Only fire callbacks if the state actually changed
if (wasSet != show)
{
TriggerRedDotCallback(type, show);
UpdateParentRedDots(type);
}
}
/// <summary>
/// Check if a badge is present.
/// </summary>
public bool HasRedDot(RedDotType type)
{
return (redDotFlags & (uint)type) != 0;
}
/// <summary>
/// Toggle a badge state.
/// </summary>
public void ToggleRedDot(RedDotType type)
{
SetRedDot(type, !HasRedDot(type));
}
/// <summary>
/// Clear all badges of given types.
/// </summary>
public void ClearRedDots(RedDotType types)
{
var originalFlags = redDotFlags;
redDotFlags &= ~(uint)types;
// Determine which badges were cleared
var clearedFlags = originalFlags & ~redDotFlags;
NotifyChangedRedDots(clearedFlags, false);
}
/// <summary>
/// Get the next badge to process (lowest set bit).
/// Use case: auto-navigation to the next feature with a notification.
/// </summary>
public RedDotType GetNextRedDot()
{
if (redDotFlags == 0) return RedDotType.None;
uint lowestBit = BitOperations.IsolateLowestSetBit(redDotFlags);
return (RedDotType)lowestBit;
}
/// <summary /// <summary>
/// Process badges one by one (auto-clear after processing)
/// Use case: batch-processing notifications.
/// </summary>
public RedDotType ProcessNextRedDot()
{
var nextRedDot = GetNextRedDot();
if (nextRedDot != RedDotType.None)
{
SetRedDot(nextRedDot, false);
}
return nextRedDot;
}
/// <summary>
/// Get number of active badges.
/// </summary>
public int GetRedDotCount()
{
return BitOperations.CountSetBits(redDotFlags);
}
/// <summary>
/// Register a callback for badge state changes.
/// </summary>
public void RegisterRedDotCallback(RedDotType type, System.Action<bool> callback)
{
if (!redDotCallbacks.ContainsKey(type))
{
redDotCallbacks[type] = new List<System.Action<bool>>();
}
redDotCallbacks[type].Add(callback);
// Fire current state immediately
callback?.Invoke(HasRedDot(type));
}
/// <summary>
/// Unregister a badge callback.
/// </summary>
public void UnregisterRedDotCallback(RedDotType type, System.Action<bool> callback)
{
if (redDotCallbacks.ContainsKey(type))
{
redDotCallbacks[type].Remove(callback);
}
}
private int GetBitIndex(RedDotType type)
{
return BitOperations.FindLowestSetBitIndex((uint)type);
}
private void TriggerRedDotCallback(RedDotType type, bool show)
{
if (redDotCallbacks.ContainsKey(type))
{
foreach (var callback in redDotCallbacks[type])
{
callback?.Invoke(show);
}
}
}
private void UpdateParentRedDots(RedDotType changedType)
{
// Update parent/group badges
if (IsQuestRedDot(changedType))
{
UpdateGroupRedDot(RedDotType.AllQuests);
}
else if (IsInventoryRedDot(changedType))
{
UpdateGroupRedDot(RedDotType.AllInventory);
}
else if (IsSocialRedDot(changedType))
{
UpdateGroupRedDot(RedDotType.AllSocial);
}
}
private void UpdateGroupRedDot(RedDotType groupType)
{
bool hasAnyChildRedDot = HasRedDot(groupType);
TriggerRedDotCallback(groupType, hasAnyChildRedDot);
}
private bool IsQuestRedDot(RedDotType type)
{
return (type & RedDotType.AllQuests) != 0;
}
private bool IsInventoryRedDot(RedDotType type)
{
return (type & RedDotType.AllInventory) != 0;
}
private bool IsSocialRedDot(RedDotType type)
{
return (type & RedDotType.AllSocial) != 0;
}
private void NotifyChangedRedDots(uint changedFlags, bool newState)
{
while (changedFlags != 0)
{
uint lowestBit = BitOperations.IsolateLowestSetBit(changedFlags);
var redDotType = (RedDotType)lowestBit;
TriggerRedDotCallback(redDotType, newState);
changedFlags = BitOperations.ClearLowestSetBit(changedFlags);
}
}
}
// UI badge component
public class RedDotUI : MonoBehaviour
{
[SerializeField] private RedDotType redDotType;
[SerializeField] private GameObject redDotIcon;
void Start()
{
RedDotManager.Instance.RegisterRedDotCallback(redDotType, OnRedDotChanged);
}
void OnDestroy()
{
if (RedDotManager.Instance != null)
{
RedDotManager.Instance.UnregisterRedDotCallback(redDotType, OnRedDotChanged);
}
}
private void OnRedDotChanged(bool hasRedDot)
{
redDotIcon.SetActive(hasRedDot);
}
}
// Usage example
public class RedDotExample : MonoBehaviour
{
void Start()
{
var redDotManager = RedDotManager.Instance;
// Set badges
redDotManager.SetRedDot(RedDotType.Mail, true);
redDotManager.SetRedDot(RedDotType.DailyQuest, true);
// Check badges
Debug.Log($"Mail badge: {redDotManager.HasRedDot(RedDotType.Mail)}");
Debug.Log($"Quest badges: {redDotManager.HasRedDot(RedDotType.AllQuests)}");
// Process badges one by one
while (redDotManager.GetRedDotCount() > 0)
{
var nextRedDot = redDotManager.ProcessNextRedDot();
Debug.Log($"Processed badge: {nextRedDot}");
}
}
}
Skill system
Bitwise operations help manage skill states, cooldowns, and condition checks.
[System.Flags]
public enum SkillState : uint
{
None = 0,
Learned = 1u << 0, // learned
OnCooldown = 1u << 1, // on cooldown
Channeling = 1u << 2, // channeling
Disabled = 1u << 3, // disabled
Enhanced = 1u << 4, // enhanced
Silenced = 1u << 5, // silenced
// Composite states
Unusable = OnCooldown | Disabled | Silenced,
Active = Learned & ~Unusable
}
[System.Flags]
public enum SkillType : uint
{
None = 0,
Physical = 1u << 0, // physical
Magical = 1u << 1, // magical
Defensive = 1u << 2, // defensive
Healing = 1u << 3, // healing
Buff = 1u << 4, // buff
Debuff = 1u << 5, // debuff
Ultimate = 1u << 6, // ultimate
Passive = 1u << 7, // passive
// Composite types
AllCombat = Physical | Magical,
AllSupport = Defensive | Healing | Buff,
AllActive = ~Passive
}
public class SkillManager : MonoBehaviour
{
[System.Serializable]
public struct SkillInfo
{
public int skillId;
public SkillType type;
public uint stateFlags;
public float cooldownTime;
public float lastUseTime;
}
private Dictionary<int, SkillInfo> skills = new Dictionary<int, SkillInfo>();
private uint globalSkillDisable = 0; // global skill-type disable flags
/// <summary>
/// Learn a skill.
/// </summary>
public void LearnSkill(int skillId, SkillType type)
{
if (skills.ContainsKey(skillId))
{
var skill = skills[skillId];
skill.stateFlags = BitOperations.SetBit(skill.stateFlags, 0); // set Learned
skills[skillId] = skill;
}
else
{
skills[skillId] = new SkillInfo
{
skillId = skillId,
type = type,
stateFlags = (uint)SkillState.Learned
};
}
}
/// <summary>
/// Check if a skill can be used.
/// </summary>
public bool CanUseSkill(int skillId)
{
if (!skills.ContainsKey(skillId)) return false;
var skill = skills[skillId];
// Check state
if ((skill.stateFlags & (uint)SkillState.Unusable) != 0) return false;
// Check global disable by type
if ((globalSkillDisable & (uint)skill.type) != 0) return false;
// Check cooldown
if (Time.time - skill.lastUseTime < skill.cooldownTime) return false;
return BitOperations.TestBit(skill.stateFlags, 0); // must be learned
}
/// <summary>
/// Use a skill.
/// </summary>
public bool UseSkill(int skillId)
{
if (!CanUseSkill(skillId)) return false;
var skill = skills[skillId];
skill.lastUseTime = Time.time;
skill.stateFlags = BitOperations.SetBit(skill.stateFlags, 1); // set OnCooldown
skills[skillId] = skill;
return true;
}
/// <summary>
/// Disable specific skill types.
/// Use cases: silence effects, special restrictions.
/// </summary>
public void DisableSkillTypes(SkillType types)
{
globalSkillDisable |= (uint)types;
}
/// <summary>
/// Enable specific skill types.
/// </summary>
public void EnableSkillTypes(SkillType types)
{
globalSkillDisable &= ~(uint)types;
}
/// <summary>
/// Get the next available skill.
/// Iterates using bitwise filters.
/// </summary>
public int GetNextAvailableSkill(SkillType typeFilter = SkillType.AllActive)
{
uint availableTypes = (uint)typeFilter & ~globalSkillDisable;
foreach (var skill in skills.Values)
{
if ((availableTypes & (uint)skill.type) != 0 && CanUseSkill(skill.skillId))
{
return skill.skillId;
}
}
return -1;
}
}
Inventory slot management
public class InventoryManager : MonoBehaviour
{
private const int MAX_SLOTS = 64;
private ulong occupiedSlots = 0; // 64-bit flags for 64 slots
/// <summary>
/// Find the first empty slot.
/// Uses "isolate lowest set bit" logic.
/// </summary>
public int FindFirstEmptySlot()
{
ulong emptySlots = ~occupiedSlots;
if (emptySlots == 0) return -1;
return BitOperations.FindLowestSetBitIndex((uint)(emptySlots & 0xFFFFFFFF));
}
/// <summary>
/// Occupy a slot.
/// </summary>
public bool OccupySlot(int slotIndex)
{
if (slotIndex < 0 || slotIndex >= MAX_SLOTS) return false;
if (IsSlotOccupied(slotIndex)) return false;
occupiedSlots |= (1ul << slotIndex);
return true;
}
/// <summary>
/// Free a slot.
/// </summary>
public void FreeSlot(int slotIndex)
{
if (slotIndex >= 0 && slotIndex < MAX_SLOTS)
{
occupiedSlots &= ~(1ul << slotIndex);
}
}
/// <summary>
/// Check if a slot is occupied.
/// </summary>
public bool IsSlotOccupied(int slotIndex)
{
return (occupiedSlots & (1ul << slotIndex)) != 0;
}
/// <summary>
/// Get the number of free slots.
/// </summary>
public int GetFreeSlotCount()
{
return MAX_SLOTS - BitOperations.CountSetBits((uint)occupiedSlots) - BitOperations.CountSetBits((uint)(occupiedSlots >> 32));
}
}
State flag management
Managing many object states with bitmasks is more efficient than scattered booleans.
using UnityEngine;
[System.Flags]
public enum CreatureState
{
None = 0,
Alive = 1 << 0, // alive
Poisoned = 1 << 1, // poisoned
Frozen = 1 << 2, // frozen
Burning = 1 << 3, // burning
Stunned = 1 << 4, // stunned
Invisible = 1 << 5, // invisible
Flying = 1 << 6, // flying
Invincible = 1 << 7, // invincible
// Composite states
Disabled = Frozen | Stunned,
ElementalDamage = Poisoned | Burning,
CantMove = Frozen | Stunned,
CantBeHurt = Invisible | Invincible
}
public class CreatureStateManager : MonoBehaviour
{
[SerializeField] private CreatureState currentState = CreatureState.Alive;
// Add a state
public void AddState(CreatureState state)
{
currentState |= state;
OnStateChanged();
}
// // Remove a state
public void RemoveState(CreatureState state)
{
currentState &= ~state;
OnStateChanged();
}
// Toggle a state
public void ToggleState(CreatureState state)
{
currentState ^= state;
OnStateChanged();
}
// Check if a specific state is set
public bool HasState(CreatureState state)
{
return (currentState & state) != 0;
}
// Check if any of the given states are set
public bool HasAnyState(CreatureState states)
{
return (currentState & states) != 0;
}
// Check if all given states are set
public bool HasAllStates(CreatureState states)
{
return (currentState & states) == states;
}
// Clear all states
public void ClearAllStates()
{
currentState = CreatureState.None;
OnStateChanged();
}
private void OnStateChanged()
{
// Update gameplay logic based on state changes
UpdateMovement();
UpdateVisual();
UpdateDamage();
}
private void UpdateMovement()
{
bool canMove = !HasAnyState(CreatureState.CantMove);
GetComponent<Rigidbody>().isKinematic = !canMove;
}
private void UpdateVisual()
{
var renderer = GetComponent<Renderer>();
if (HasState(CreatureState.Invisible))
{
renderer.material.color = new Color(1, 1, 1, 0.3f);
}
else if (HasState(CreatureState.Burning))
{
renderer.material.color = Color.red;
}
else if (HasState(CreatureState.Frozen))
{
renderer.material.color = Color.cyan;
}
else
{
renderer.material.color = Color.white;
}
}
private void UpdateDamage()
{
if (HasAnyState(CreatureState.CantBeHurt))
{
// Set to an invincible layer
gameObject.layer = LayerMask.NameToLayer("Invincible");
}
else
{
gameObject.layer = LayerMask.NameToLayer("Damageable");
}
}
void Update()
{
// Example: damage over time
if (HasState(CreatureState.Poisoned))
{
TakeDamage(1 * Time.deltaTime);
}
if (HasState(CreatureState.Burning))
{
TakeDamage(2 * Time.deltaTime);
}
}
private void TakeDamage(float damage)
{
Debug.Log($"Took damage: {damage}");
}
}
Permission system
Bitmasks are great for permission checks—fast and compact.
[System.Flags]
public enum UserPermissions
{
None = 0,
Read = 1 << 0, // read
Write = 1 << 1, // write
Execute = 1 << 2, // execute
Delete = 1 << 3, // delete
Admin = 1 << 4, // admin
Moderator = 1 << 5, // moderator
// Composite roles
BasicUser = Read,
PowerUser = Read | Write | Execute,
SuperAdmin = Read | Write | Execute | Delete | Admin,
FullControl = ~0 // all permissions
}
public class PermissionSystem : MonoBehaviour
{
[SerializeField] private UserPermissions userPermissions;
public bool HasPermission(UserPermissions permission)
{
return (userPermissions & permission) == permission;
}
public void GrantPermission(UserPermissions permission)
{
userPermissions |= permission;
Debug.Log($"Granted: {permission}");
}
public void RevokePermission(UserPermissions permission)
{
userPermissions &= ~permission;
Debug.Log($"Revoked: {permission}");
}
public bool TryExecuteAction(UserPermissions requiredPermission, System.Action action)
{
if (HasPermission(requiredPermission))
{
action?.Invoke();
return true;
}
else
{
Debug.LogWarning($"Insufficient permission, required: {requiredPermission}");
return false;
}
}
void Start()
{
// Examples
TryExecuteAction(UserPermissions.Read, () => Debug.Log("Read file"));
TryExecuteAction(UserPermissions.Delete, () => Debug.Log("Delete file"));
// Check combined permissions
if (HasPermission(UserPermissions.Read | UserPermissions.Write))
{
Debug.Log("Read and write permitted");
}
}
}
以下为英文翻译(保留结构与代码,代码中的注释与日志文本已翻译为英文):
Event Flag System
Use bitmasks to manage event subscriptions and triggers in the game.
[System.Flags]
public enum GameEvents
{
None = 0,
PlayerDied = 1 << 0,
EnemyKilled = 1 << 1,
ItemCollected = 1 << 2,
LevelCompleted = 1 << 3,
BossDefeated = 1 << 4,
PowerUpActivated = 1 << 5,
// Event combinations
CombatEvents = EnemyKilled | BossDefeated,
ProgressEvents = LevelCompleted | ItemCollected,
AllEvents = ~0
}
public class EventManager : MonoBehaviour
{
private static EventManager instance;
public static EventManager Instance => instance;
private System.Collections.Generic.Dictionary<GameEvents, System.Action> eventHandlers
= new System.Collections.Generic.Dictionary<GameEvents, System.Action>();
void Awake()
{
if (instance == null)
{
instance = this;
DontDestroyOnLoad(gameObject);
}
else
{
Destroy(gameObject);
}
}
public void Subscribe(GameEvents events, System.Action handler)
{
foreach (GameEvents eventType in System.Enum.GetValues(typeof(GameEvents)))
{
if (eventType != GameEvents.None && (events & eventType) != 0)
{
if (!eventHandlers.ContainsKey(eventType))
{
eventHandlers[eventType] = null;
}
eventHandlers[eventType] += handler;
}
}
}
public void Unsubscribe(GameEvents events, System.Action handler)
{
foreach (GameEvents eventType in System.Enum.GetValues(typeof(GameEvents)))
{
if (eventType != GameEvents.None && (events & eventType) != 0)
{
if (eventHandlers.ContainsKey(eventType))
{
eventHandlers[eventType] -= handler;
}
}
}
}
public void TriggerEvent(GameEvents eventType)
{
if (eventHandlers.ContainsKey(eventType))
{
eventHandlers[eventType]?.Invoke();
}
}
// Trigger multiple events
public void TriggerEvents(GameEvents events)
{
foreach (GameEvents eventType in System.Enum.GetValues(typeof(GameEvents)))
{
if (eventType != GameEvents.None && (events & eventType) != 0)
{
TriggerEvent(eventType);
}
}
}
}
// Usage example
public class GameEventListener : MonoBehaviour
{
void Start()
{
// Subscribe to combat-related events
EventManager.Instance.Subscribe(GameEvents.CombatEvents, OnCombatEvent);
// Subscribe to a specific event
EventManager.Instance.Subscribe(GameEvents.ItemCollected, OnItemCollected);
}
void OnDestroy()
{
if (EventManager.Instance != null)
{
EventManager.Instance.Unsubscribe(GameEvents.CombatEvents, OnCombatEvent);
EventManager.Instance.Unsubscribe(GameEvents.ItemCollected, OnItemCollected);
}
}
private void OnCombatEvent()
{
Debug.Log("Combat event triggered");
}
private void OnItemCollected()
{
Debug.Log("Item collected event triggered");
}
}
Optimizing Boolean Collections
Pack multiple related booleans into a single integer to save memory and improve access speed.
public class OptimizedBooleanCollection
{
private int flags = 0;
// Set the boolean value at the specified index
public void SetFlag(int index, bool value)
{
if (value)
{
flags |= (1 << index);
}
else
{
flags &= ~(1 << index);
}
}
// Get the boolean value at the specified index
public bool GetFlag(int index)
{
return (flags & (1 << index)) != 0;
}
// Toggle the boolean value at the specified index
public void ToggleFlag(int index)
{
flags ^= (1 << index);
}
// Clear all flags
public void ClearAll()
{
flags = 0;
}
// Set all flags
public void SetAll()
{
flags = ~0;
}
// Get the number of flags set to true
public int GetTrueCount()
{
return BitUtils.CountSetBits(flags);
}
// Check if any flag is true
public bool HasAnyTrue()
{
return flags != 0;
}
// Check if all flags are false
public bool AllFalse()
{
return flags == 0;
}
}
// Usage example: Level progress management
public class LevelProgressManager : MonoBehaviour
{
private OptimizedBooleanCollection levelCompleted = new OptimizedBooleanCollection();
private OptimizedBooleanCollection levelUnlocked = new OptimizedBooleanCollection();
public void CompleteLevel(int levelIndex)
{
levelCompleted.SetFlag(levelIndex, true);
// Unlock the next level
if (levelIndex + 1 < 32) // Assume up to 32 levels
{
levelUnlocked.SetFlag(levelIndex + 1, true);
}
Debug.Log($"Level {levelIndex} completed! Completed levels: {levelCompleted.GetTrueCount()}");
}
public bool IsLevelCompleted(int levelIndex)
{
return levelCompleted.GetFlag(levelIndex);
}
public bool IsLevelUnlocked(int levelIndex)
{
return levelUnlocked.GetFlag(levelIndex);
}
public float GetProgressPercentage()
{
return levelCompleted.GetTrueCount() / 32f * 100f;
}
}
Collision Detection Optimization
Use bitmasks to optimize collision logic between objects.
[System.Flags]
public enum CollisionCategory
{
None = 0,
Player = 1 << 0,
Enemy = 1 << 1,
Projectile = 1 << 2,
Wall = 1 << 3,
Ground = 1 << 4,
PowerUp = 1 << 5,
Trigger = 1 << 6
}
public class CollisionController : MonoBehaviour
{
[SerializeField] private CollisionCategory myCategory;
[SerializeField] private CollisionCategory collidesWith;
private void OnCollisionEnter(Collision collision)
{
var otherController = collision.gameObject.GetComponent<CollisionController>();
if (otherController != null)
{
if (CanCollideWith(otherController.myCategory))
{
HandleCollision(otherController);
}
}
}
private bool CanCollideWith(CollisionCategory otherCategory)
{
return (collidesWith & otherCategory) != 0;
}
private void HandleCollision(CollisionController other)
{
Debug.Log($"{myCategory} collided with {other.myCategory}");
// Execute different logic based on collision type
if (myCategory == CollisionCategory.Player && other.myCategory == CollisionCategory.PowerUp)
{
CollectPowerUp(other.gameObject);
}
else if (myCategory == CollisionCategory.Projectile && other.myCategory == CollisionCategory.Enemy)
{
DamageEnemy(other.gameObject);
}
}
private void CollectPowerUp(GameObject powerUp)
{
Debug.Log("Collect power-up");
Destroy(powerUp);
}
private void DamageEnemy(GameObject enemy)
{
Debug.Log("Damage enemy");
// Damage logic
}
}
Advanced Techniques
Bitmask Serialization
Properly serialize bitmask data in Unity.
[System.Serializable]
public class SerializableBitMask
{
[SerializeField] private int mask;
public bool this[int index]
{
get => (mask & (1 << index)) != 0;
set
{
if (value)
mask |= (1 << index);
else
mask &= ~(1 << index);
}
}
public void SetMask(int newMask) => mask = newMask;
public int GetMask() => mask;
public void Clear() => mask = 0;
public override string ToString()
{
return System.Convert.ToString(mask, 2).PadLeft(32, '0');
}
}
// Custom PropertyDrawer for displaying in the Inspector
#if UNITY_EDITOR
using UnityEditor;
[CustomPropertyDrawer(typeof(SerializableBitMask))]
public class SerializableBitMaskDrawer : PropertyDrawer
{
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
var maskProperty = property.FindPropertyRelative("mask");
EditorGUI.BeginProperty(position, label, property);
var rect = new Rect(position.x, position.y, position.width, EditorGUIUtility.singleLineHeight);
EditorGUI.PropertyField(rect, maskProperty, label);
rect.y += EditorGUIUtility.singleLineHeight + 2;
var mask = maskProperty.intValue;
var binaryString = System.Convert.ToString(mask, 2).PadLeft(16, '0');
EditorGUI.LabelField(rect, "Binary:", binaryString);
EditorGUI.EndProperty();
}
public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
{
return EditorGUIUtility.singleLineHeight * 2 + 2;
}
}
#endif
Dynamic Bitmask Manager
Implement a general-purpose bitmask management system.
public class DynamicBitMaskManager<T> where T : System.Enum
{
private int mask = 0;
public void Add(T flag)
{
mask |= ConvertToInt(flag);
}
public void Remove(T flag)
{
mask &= ~ConvertToInt(flag);
}
public void Toggle(T flag)
{
mask ^= ConvertToInt(flag);
}
public bool Has(T flag)
{
return (mask & ConvertToInt(flag)) != 0;
}
public bool HasAny(params T[] flags)
{
foreach (var flag in flags)
{
if (Has(flag)) return true;
}
return false;
}
public bool HasAll(params T[] flags)
{
foreach (var flag in flags)
{
if (!Has(flag)) return false;
}
return true;
}
public void Clear()
{
mask = 0;
}
public T[] GetActiveFlags()
{
var activeFlags = new System.Collections.Generic.List<T>();
foreach (T flag in System.Enum.GetValues(typeof(T)))
{
if (Has(flag))
{
activeFlags.Add(flag);
}
}
return activeFlags.ToArray();
}
private int ConvertToInt(T flag)
{
return System.Convert.ToInt32(flag);
}
public override string ToString()
{
return string.Join(", ", GetActiveFlags());
}
}
// Usage example
public class DynamicMaskExample : MonoBehaviour
{
private DynamicBitMaskManager<CreatureState> stateManager;
void Start()
{
stateManager = new DynamicBitMaskManager<CreatureState>();
// Add states
stateManager.Add(CreatureState.Alive);
stateManager.Add(CreatureState.Flying);
// Check states
Debug.Log("Alive: " + stateManager.Has(CreatureState.Alive));
Debug.Log("Current states: " + stateManager.ToString());
// Toggle state
stateManager.Toggle(CreatureState.Invisible);
Debug.Log("After toggling invisibility: " + stateManager.ToString());
}
}
Performance Optimization
Bitwise Operation Performance Tests
using UnityEngine;
using System.Diagnostics;
public class BitOperationPerformanceTest : MonoBehaviour
{
private const int ITERATIONS = 1000000;
void Start()
{
TestBooleanArrayVsBitMask();
TestFlagEnumPerformance();
}
void TestBooleanArrayVsBitMask()
{
// Test boolean array
bool[] boolArray = new bool[32];
var sw = Stopwatch.StartNew();
for (int i = 0; i < ITERATIONS; i++)
{
boolArray[i % 32] = !boolArray[i % 32];
}
sw.Stop();
long boolArrayTime = sw.ElapsedMilliseconds;
// Test bitmask
int bitMask = 0;
sw.Restart();
for (int i = 0; i < ITERATIONS; i++)
{
bitMask ^= (1 << (i % 32));
}
sw.Stop();
long bitMaskTime = sw.ElapsedMilliseconds;
UnityEngine.Debug.Log($"Boolean array elapsed: {boolArrayTime}ms");
UnityEngine.Debug.Log($"Bitmask elapsed: {bitMaskTime}ms");
UnityEngine.Debug.Log($"Speedup: {(float)boolArrayTime / bitMaskTime:F2}x");
}
void TestFlagEnumPerformance()
{
CreatureState state = CreatureState.Alive;
var sw = Stopwatch.StartNew();
for (int i = 0; i < ITERATIONS; i++)
{
// Add state
state |= CreatureState.Flying;
// Check state
bool hasFlying = (state & CreatureState.Flying) != 0;
// Remove state
state &= ~CreatureState.Flying;
}
sw.Stop();
UnityEngine.Debug.Log($"Flag enum operations elapsed: {sw.ElapsedMilliseconds}ms");
}
}
Bitwise Operation Benchmark
public class BitOperationBenchmark : MonoBehaviour
{
void Start()
{
const int iterations = 1000000;
// Test red dot system performance
var sw = System.Diagnostics.Stopwatch.StartNew();
uint redDots = 0;
for (int i = 0; i < iterations; i++)
{
// Set bit
redDots = BitOperations.SetBit(redDots, i % 32);
// Test bit
bool hasRedDot = BitOperations.TestBit(redDots, i % 32);
// Clear bit
if (i % 10 == 0) redDots = BitOperations.ClearBit(redDots, i % 32);
}
sw.Stop();
Debug.Log($"Bitwise red dot system {iterations} operations took: {sw.ElapsedMilliseconds}ms");
// Compare with traditional Dictionary approach
var redDotDict = new Dictionary<int, bool>();
sw.Restart();
for (int i = 0; i < iterations; i++)
{
redDotDict[i % 32] = true;
bool hasRedDot = redDotDict.ContainsKey(i % 32) && redDotDict[i % 32];
if (i % 10 == 0) redDotDict[i % 32] = false;
}
sw.Stop();
Debug.Log($"Dictionary red dot system {iterations} operations took: {sw.ElapsedMilliseconds}ms");
}
}
Unity Bitwise Memory Optimization: Complete Example
Memory Optimization Scenario Examples
Map Region Management System
In large open-world games, you need to manage the state of tens of thousands of map regions (explored, has enemies, has treasure, etc.).
using UnityEngine;
using System.Collections.Generic;
using System;
[System.Flags]
public enum MapRegionState : byte
{
None = 0,
Explored = 1 << 0, // Explored
HasEnemies = 1 << 1, // Has enemies
HasTreasure = 1 << 2, // Has treasure
HasNPC = 1 << 3, // Has NPC
IsLocked = 1 << 4, // Region locked
IsDangerous = 1 << 5, // Dangerous area
HasQuest = 1 << 6, // Has quest
IsBossArea = 1 << 7 // Boss area
}
// Traditional approach - high memory usage
public class TraditionalMapManager : MonoBehaviour
{
[System.Serializable]
public class RegionData
{
public bool explored;
public bool hasEnemies;
public bool hasTreasure;
public bool hasNPC;
public bool isLocked;
public bool isDangerous;
public bool hasQuest;
public bool isBossArea;
// Each region needs 8 bytes (8 bools)
}
private Dictionary<int, RegionData> regions = new Dictionary<int, RegionData>();
// 10,000 regions = 10,000 * (8 bytes + Dictionary overhead) ≈ 240KB+
}
// Optimized approach - using bitwise operations
public class OptimizedMapManager : MonoBehaviour
{
private Dictionary<int, byte> regionStates = new Dictionary<int, byte>();
// 10,000 regions = 10,000 * 1 byte ≈ 10KB (95%+ memory savings)
private Dictionary<Vector2Int, int> positionToRegionId = new Dictionary<Vector2Int, int>();
private Dictionary<int, Vector2Int> regionIdToPosition = new Dictionary<int, Vector2Int>();
/// <summary>
/// Set region state
/// </summary>
public void SetRegionState(int regionId, MapRegionState state, bool value)
{
if (!regionStates.ContainsKey(regionId))
{
regionStates[regionId] = 0;
}
byte currentState = regionStates[regionId];
if (value)
{
currentState |= (byte)state;
}
else
{
currentState &= (byte)~state;
}
regionStates[regionId] = currentState;
}
/// <summary>
/// Check region state
/// </summary>
public bool HasRegionState(int regionId, MapRegionState state)
{
if (!regionStates.ContainsKey(regionId)) return false;
return (regionStates[regionId] & (byte)state) != 0;
}
/// <summary>
/// Get all region states
/// </summary>
public MapRegionState GetAllRegionStates(int regionId)
{
if (!regionStates.ContainsKey(regionId)) return MapRegionState.None;
return (MapRegionState)regionStates[regionId];
}
/// <summary>
/// Set multiple states in batch
/// </summary>
public void SetMultipleStates(int regionId, MapRegionState states)
{
regionStates[regionId] = (byte)states;
}
/// <summary>
/// Check if any of the specified states are present
/// </summary>
public bool HasAnyState(int regionId, MapRegionState statesMask)
{
if (!regionStates.ContainsKey(regionId)) return false;
return (regionStates[regionId] & (byte)statesMask) != 0;
}
/// <summary>
/// Find all regions with a specific state
/// </summary>
public List<int> FindRegionsWithState(MapRegionState state)
{
var result = new List<int>();
byte stateByte = (byte)state;
foreach (var kvp in regionStates)
{
if ((kvp.Value & stateByte) != 0)
{
result.Add(kvp.Key);
}
}
return result;
}
/// <summary>
/// Get the number of regions with a specific state
/// </summary>
public int CountRegionsWithState(MapRegionState state)
{
int count = 0;
byte stateByte = (byte)state;
foreach (var stateValue in regionStates.Values)
{
if ((stateValue & stateByte) != 0)
{
count++;
}
}
return count;
}
/// <summary>
/// Serialize map data - extremely compact
/// </summary>
[System.Serializable]
public struct CompactMapData
{
public int[] regionIds;
public byte[] states;
public int GetDataSize()
{
return regionIds.Length * 4 + states.Length; // int(4 bytes) + byte(1 byte)
}
}
public CompactMapData SerializeMapData()
{
var data = new CompactMapData
{
regionIds = new int[regionStates.Count],
states = new byte[regionStates.Count]
};
int index = 0;
foreach (var kvp in regionStates)
{
data.regionIds[index] = kvp.Key;
data.states[index] = kvp.Value;
index++;
}
return data;
}
public void DeserializeMapData(CompactMapData data)
{
regionStates.Clear();
for (int i = 0; i < data.regionIds.Length; i++)
{
regionStates[data.regionIds[i]] = data.states[i];
}
}
}
Achievement System Memory Optimization
[System.Flags]
public enum AchievementCategory : uint
{
None = 0,
Combat = 1u << 0,
Exploration = 1u << 1,
Social = 1u << 2,
Collection = 1u << 3,
Story = 1u << 4,
Crafting = 1u << 5,
Trading = 1u << 6,
PVP = 1u << 7,
// Combined categories
AllPVE = Combat | Exploration | Story,
AllSocial = Social | Trading,
AllCreative = Crafting | Collection
}
public class CompactAchievementManager : MonoBehaviour
{
// Traditional approach: one object per achievement with many fields
// 1000 achievements * ~100 bytes each = 100KB
// Optimized approach: compact storage
[System.Serializable]
public struct AchievementData
{
public ushort achievementId; // 2 bytes - supports up to 65,536 achievements
public byte progress; // 1 byte - progress percentage 0-100
public byte flags; // 1 byte - status flags
// Total only 4 bytes per achievement
}
[System.Flags]
private enum AchievementFlags : byte
{
None = 0,
Unlocked = 1 << 0, // Unlocked
Completed = 1 << 1, // Completed
Claimed = 1 << 2, // Reward claimed
Hidden = 1 << 3, // Hidden achievement
Pinned = 1 << 4, // Pinned
New = 1 << 5, // Newly obtained
Favorite = 1 << 6, // Favorited
Shared = 1 << 7 // Shared
}
private List<AchievementData> achievements = new List<AchievementData>();
private Dictionary<ushort, int> achievementIndexMap = new Dictionary<ushort, int>();
// Category cache - store which achievements each category contains using bits
private Dictionary<AchievementCategory, uint[]> categoryMasks = new Dictionary<AchievementCategory, uint[]>();
/// <summary>
/// Add an achievement
/// </summary>
public void AddAchievement(ushort achievementId, AchievementCategory category)
{
if (achievementIndexMap.ContainsKey(achievementId)) return;
var achievementData = new AchievementData
{
achievementId = achievementId,
progress = 0,
flags = (byte)AchievementFlags.None
};
int index = achievements.Count;
achievements.Add(achievementData);
achievementIndexMap[achievementId] = index;
// Update category mask
UpdateCategoryMask(category, index, true);
}
/// <summary>
/// Set achievement progress
/// </summary>
public void SetAchievementProgress(ushort achievementId, byte progress)
{
if (!achievementIndexMap.TryGetValue(achievementId, out int index)) return;
var achievement = achievements[index];
achievement.progress = progress;
// Auto-complete check
if (progress >= 100)
{
achievement.flags |= (byte)AchievementFlags.Completed;
achievement.flags |= (byte)AchievementFlags.New;
}
achievements[index] = achievement;
}
/// <summary>
/// Set an achievement flag
/// </summary>
public void SetAchievementFlag(ushort achievementId, AchievementFlags flag, bool value)
{
if (!achievementIndexMap.TryGetValue(achievementId, out int index)) return;
var achievement = achievements[index];
if (value)
{
achievement.flags |= (byte)flag;
}
else
{
achievement.flags &= (byte)~flag;
}
achievements[index] = achievement;
}
/// <summary>
/// Check an achievement flag
/// </summary>
public bool HasAchievementFlag(ushort achievementId, AchievementFlags flag)
{
if (!achievementIndexMap.TryGetValue(achievementId, out int index)) return false;
return (achievements[index].flags & (byte)flag) != 0;
}
/// <summary>
/// Get the number of completed achievements in a category
/// </summary>
public int GetCompletedCountInCategory(AchievementCategory category)
{
if (!categoryMasks.TryGetValue(category, out uint[] masks)) return 0;
int count = 0;
for (int maskIndex = 0; maskIndex < masks.Length; maskIndex++)
{
uint mask = masks[maskIndex];
uint currentBit = 1;
for (int bit = 0; bit < 32 && maskIndex * 32 + bit < achievements.Count; bit++)
{
if ((mask & currentBit) != 0)
{
int achievementIndex = maskIndex * 32 + bit;
if ((achievements[achievementIndex].flags & (byte)AchievementFlags.Completed) != 0)
{
count++;
}
}
currentBit <<= 1;
}
}
return count;
}
/// <summary>
/// Get the list of achievements that need a red dot (notification)
/// </summary>
public List<ushort> GetAchievementsWithRedDot()
{
var result = new List<ushort>();
byte redDotMask = (byte)(AchievementFlags.New | AchievementFlags.Completed) & ~(byte)AchievementFlags.Claimed;
for (int i = 0; i < achievements.Count; i++)
{
if ((achievements[i].flags & redDotMask) != 0)
{
result.Add(achievements[i].achievementId);
}
}
return result;
}
/// <summary>
/// Batch process new achievements
/// </summary>
public void ProcessNewAchievements()
{
for (int i = 0; i < achievements.Count; i++)
{
var achievement = achievements[i];
if ((achievement.flags & (byte)AchievementFlags.New) != 0)
{
// Handle new achievement logic
Debug.Log($"New Achievement: {achievement.achievementId}");
// Clear 'New' flag
achievement.flags &= (byte)~AchievementFlags.New;
achievements[i] = achievement;
}
}
}
private void UpdateCategoryMask(AchievementCategory category, int achievementIndex, bool add)
{
if (!categoryMasks.ContainsKey(category))
{
int maskArraySize = (achievements.Capacity + 31) / 32; // rounded up
categoryMasks[category] = new uint[maskArraySize];
}
uint[] masks = categoryMasks[category];
int maskIndex = achievementIndex / 32;
int bitIndex = achievementIndex % 32;
if (add)
{
masks[maskIndex] |= (1u << bitIndex);
}
else
{
masks[maskIndex] &= ~(1u << bitIndex);
}
}
/// <summary>
/// Get memory usage stats
/// </summary>
public void LogMemoryUsage()
{
int achievementDataSize = achievements.Count * 4; // 4 bytes per achievement
int indexMapSize = achievementIndexMap.Count * 8; // estimated Dictionary overhead
int categoryMaskSize = 0;
foreach (var masks in categoryMasks.Values)
{
categoryMaskSize += masks.Length * 4; // 4 bytes per uint
}
int totalSize = achievementDataSize + indexMapSize + categoryMaskSize;
Debug.Log($"Achievement system memory usage:");
Debug.Log($" Achievement data: {achievementDataSize} bytes ({achievements.Count} achievements)");
Debug.Log($" Index map: {indexMapSize} bytes");
Debug.Log($" Category masks: {categoryMaskSize} bytes");
Debug.Log($" Total: {totalSize} bytes ({totalSize / 1024f:F2} KB)");
// Compare with traditional approach
int traditionalSize = achievements.Count * 100; // estimated traditional object size
float savings = (1f - (float)totalSize / traditionalSize) * 100f;
Debug.Log($" Savings compared to traditional approach: {savings:F1}% ({traditionalSize - totalSize} bytes)");
}
/// <summary>
/// Serialize achievement data into a compact format
/// </summary>
public byte[] SerializeToBytes()
{
var buffer = new byte[achievements.Count * 4];
int offset = 0;
foreach (var achievement in achievements)
{
// 2 bytes for ID
buffer[offset] = (byte)(achievement.achievementId & 0xFF);
buffer[offset + 1] = (byte)(achievement.achievementId >> 8);
// 1 byte for progress
buffer[offset + 2] = achievement.progress;
// 1 byte for flags
buffer[offset + 3] = achievement.flags;
offset += 4;
}
return buffer;
}
/// <summary>
/// Deserialize from compact format
/// </summary>
public void DeserializeFromBytes(byte[] data)
{
achievements.Clear();
achievementIndexMap.Clear();
for (int i = 0; i < data.Length; i += 4)
{
var achievement = new AchievementData
{
achievementId = (ushort)(data[i] | (data[i + 1] << 8)),
progress = data[i + 2],
flags = data[i + 3]
};
int index = achievements.Count;
achievements.Add(achievement);
achievementIndexMap[achievement.achievementId] = index;
}
}
}
// Usage example and performance test
public class MemoryOptimizationExample : MonoBehaviour
{
void Start()
{
var mapManager = new OptimizedMapManager();
var achievementManager = new CompactAchievementManager();
// Create lots of test data
for (int i = 0; i < 10000; i++)
{
// Map regions
var randomStates = (MapRegionState)(UnityEngine.Random.Range(1, 256));
mapManager.SetMultipleStates(i, randomStates);
// Achievements
if (i < 1000)
{
achievementManager.AddAchievement((ushort)i, AchievementCategory.Combat);
achievementManager.SetAchievementProgress((ushort)i, (byte)UnityEngine.Random.Range(0, 101));
}
}
// Output memory usage stats
achievementManager.LogMemoryUsage();
Debug.Log($"Map system: {mapManager.CountRegionsWithState(MapRegionState.Explored)} explored regions");
Debug.Log($"Achievement system: {achievementManager.GetCompletedCountInCategory(AchievementCategory.Combat)} completed combat achievements");
}
}
Through these memory optimization examples, we can see the tremendous value of bitwise operations in game development:
- Map system: from 8 bytes per region down to 1 byte, saving 87.5% memory
- Achievement system: from ~100 bytes per achievement down to 4 bytes, saving 96% memory
- Serialization efficiency: more compact data, more efficient transfer and storage
- Query performance: bitwise operations are faster than accessing object fields
Conclusion
It’s also a reminder to myself to lose my temper less (even if you really run into idiots, just giggle), keep an open mind and strive for excellence, and aim to nail it in the next explanation. Have you encountered any awkward situations during interviews? Feel free to share in the comments!
Top comments (0)