Code/BehaviorTree/Blackboard.cs
using System.Collections.Generic;

namespace NPBehave
{
    public class Blackboard
    {
        public enum Type
        {
            Add,
            Remove,
            Change
        }
        private struct Notification
        {
            public string Key;
            public Type Type;
            public object Value;
            public Notification(string key, Type type, object value)
            {
                Key = key;
                Type = type;
                Value = value;
            }
        }

        private Clock _clock;
        private Dictionary<string, object> _data = new Dictionary<string, object>();
        private Dictionary<string, List<System.Action<Type, object>>> _observers = new Dictionary<string, List<System.Action<Type, object>>>();
        private bool _isNotifiyng = false;
        private Dictionary<string, List<System.Action<Type, object>>> _addObservers = new Dictionary<string, List<System.Action<Type, object>>>();
        private Dictionary<string, List<System.Action<Type, object>>> _removeObservers = new Dictionary<string, List<System.Action<Type, object>>>();
        private List<Notification> _notifications = new List<Notification>();
        private List<Notification> _notificationsDispatch = new List<Notification>();
        private Blackboard _parentBlackboard;
        private HashSet<Blackboard> _children = new HashSet<Blackboard>();

        public Blackboard(Blackboard parent, Clock clock)
        {
            _clock = clock;
            _parentBlackboard = parent;
        }
        public Blackboard(Clock clock)
        {
            _parentBlackboard = null;
            _clock = clock;
        }

        public void Enable()
        {
	        _parentBlackboard?._children.Add(this);
        }

        public void Disable()
        {
	        _parentBlackboard?._children.Remove(this);
            if (_clock != null)
            {
                _clock.RemoveTimer(NotifiyObservers);
            }
        }

        public object this[string key]
        {
            get
            {
                return Get(key);
            }
            set
            {
                Set(key, value);
            }
        }

        public void Set(string key)
        {
            if (!IsSet(key))
            {
                Set(key, null);
            }
        }

        public void Set(string key, object value)
        {
            if (_parentBlackboard != null && _parentBlackboard.IsSet(key))
            {
                _parentBlackboard.Set(key, value);
            }
            else
            {
                if (_data.TryAdd(key, value))
                {
	                _notifications.Add(new Notification(key, Type.Add, value));
                    _clock.AddTimer(0f, 0, NotifiyObservers);
                }
                else
                {
                    if ((_data[key] == null && value != null) || (_data[key] != null && !_data[key].Equals(value)))
                    {
                        _data[key] = value;
                        _notifications.Add(new Notification(key, Type.Change, value));
                        _clock.AddTimer(0f, 0, NotifiyObservers);
                    }
                }
            }
        }

        public void Unset(string key)
        {
            if (_data.ContainsKey(key))
            {
                _data.Remove(key);
                _notifications.Add(new Notification(key, Type.Remove, null));
                _clock.AddTimer(0f, 0, NotifiyObservers);
            }
        }

        public T Get<T>(string key)
        {
            object result = Get(key);
            if (result == null)
            {
                return default(T);
            }
            return (T)result;
        }

        public object Get(string key)
        {
	        return _data.TryGetValue(key, out var value) ? value : _parentBlackboard?.Get(key);
        }

        public bool IsSet(string key)
        {
            return _data.ContainsKey(key) || (_parentBlackboard != null && _parentBlackboard.IsSet(key));
        }

        public void AddObserver(string key, System.Action<Type, object> observer)
        {
            List<System.Action<Type, object>> observers = GetObserverList(_observers, key);
            if (!_isNotifiyng)
            {
                if (!observers.Contains(observer))
                {
                    observers.Add(observer);
                }
            }
            else
            {
                if (!observers.Contains(observer))
                {
                    List<System.Action<Type, object>> addObservers = GetObserverList(_addObservers, key);
                    if (!addObservers.Contains(observer))
                    {
                        addObservers.Add(observer);
                    }
                }

                List<System.Action<Type, object>> removeObservers = GetObserverList(_removeObservers, key);
                if (removeObservers.Contains(observer))
                {
                    removeObservers.Remove(observer);
                }
            }
        }

        public void RemoveObserver(string key, System.Action<Type, object> observer)
        {
            List<System.Action<Type, object>> observers = GetObserverList(_observers, key);
            if (!_isNotifiyng)
            {
                if (observers.Contains(observer))
                {
                    observers.Remove(observer);
                }
            }
            else
            {
                List<System.Action<Type, object>> removeObservers = GetObserverList(_removeObservers, key);
                if (!removeObservers.Contains(observer))
                {
                    if (observers.Contains(observer))
                    {
                        removeObservers.Add(observer);
                    }
                }

                List<System.Action<Type, object>> addObservers = GetObserverList(_addObservers, key);
                if (addObservers.Contains(observer))
                {
                    addObservers.Remove(observer);
                }
            }
        }


#if DEBUG
        public List<string> Keys
        {
            get
            {
                if (_parentBlackboard != null)
                {
                    List<string> keys = this._parentBlackboard.Keys;
                    keys.AddRange(_data.Keys);
                    return keys;
                }
                else
                {
                    return new List<string>(_data.Keys);
                }
            }
        }

        public int NumObservers
        {
            get
            {
                int count = 0;
                foreach (var key in _observers.Keys)
                {
                    count += _observers[key].Count;
                }
                return count;
            }
        }
#endif


        private void NotifiyObservers()
        {
            if (_notifications.Count == 0)
            {
                return;
            }

            _notificationsDispatch.Clear();
            _notificationsDispatch.AddRange(_notifications);
            foreach (Blackboard child in _children)
            {
                child._notifications.AddRange(_notifications);
                child._clock.AddTimer(0f, 0, child.NotifiyObservers);
            }
            _notifications.Clear();

            _isNotifiyng = true;
            foreach (Notification notification in _notificationsDispatch)
            {
                if (!_observers.ContainsKey(notification.Key))
                {
                    //                Debug.Log("1 do not notify for key:" + notification.key + " value: " + notification.value);
                    continue;
                }

                List<System.Action<Type, object>> observers = GetObserverList(_observers, notification.Key);
                foreach (System.Action<Type, object> observer in observers)
                {
                    if (_removeObservers.TryGetValue( notification.Key, out List<System.Action<Type, object>> value ) && value.Contains(observer))
                    {
                        continue;
                    }
                    observer(notification.Type, notification.Value);
                }
            }

            foreach (string key in _addObservers.Keys)
            {
                GetObserverList(_observers, key).AddRange(_addObservers[key]);
            }
            foreach (string key in _removeObservers.Keys)
            {
                foreach (System.Action<Type, object> action in _removeObservers[key])
                {
                    GetObserverList(_observers, key).Remove(action);
                }
            }
            _addObservers.Clear();
            _removeObservers.Clear();

            _isNotifiyng = false;
        }

        private List<System.Action<Type, object>> GetObserverList(Dictionary<string, List<System.Action<Type, object>>> target, string key)
        {
            List<System.Action<Type, object>> observers;
            if (target.TryGetValue(key, out var value))
            {
                observers = value;
            }
            else
            {
                observers = new List<System.Action<Type, object>>();
                target[key] = observers;
            }
            return observers;
        }
    }
}