Tips and Tricks 1: отложенные вызовы функций (Functor Manager)

    При создании игр разработчики часто сталкиваются с такой задачей — какое событие должно быть выполнено, но не в данный момент, а спустя какое-то время. Решается эта задача по разному. Чаще всего у игровых объектов присутствует собственный внутренний таймер, и нужную задержку можно реализовать с его помощью (добавив лишний код объекту). Но иногда нужно сделать отложенный вызов метода у объекта, не имеющего собственного таймера, скажем скрыть окно, строку, показать иконку или эффектик спустя некоторое время, сделать что-то ещё, но не прямо сейчас, а с задержкой. Однократно или несколько раз.

    Вот для таких и подобных целей моим коллегой была разработана универсальная система, которую он назвал Functor Manager (возможно для названия подобных систем есть устоявшийся другой термин, я не знаю, буду рад если подскажут).

    Изначально код был написан на C++, и использовался в наших прошлых проектах, сейчас у нас проект на C#, поэтому реализацию приведу на C#. Краткая концепция и код реализации (C#) под катом.



    Functor



    Фанктор — это классик, который помнит, через сколько времени и какую функцию надо вызвать.

    Итак, собственно фанктор состоит из:
    Guid — уникального идентификатора, который генерируется при создании фанктора. Идентификатор этот нужен чтобы можно было удалить фанктор в случае ненадобности.
    _deltaTime — внутренний таймер. Туда записывается время, через которое должен произойти вызов функции.
    _func — указатель на функцию, код которой должен быть выполнен по истечению времени таймера. Функция возвращает значение — через какой период (в секундах) фанктор должен повторить вызов метода. В случае, если функция вернула значение 0.0f, то фанктор прибивается.
    _funcArg — аргумент функции. Сюда можно передать скажем указатель на объект, или любую другую нужную информацию.

    В коде фанктора всё просто — конструкторы, заполняющие поля, и единственный метод, который стоит пояснения — это метод Process. Счётчик уменьшается на количество тиков, если достиг нуля — вызывается функция. Возвращаемое значение присваивается таймеру.

    Код фанктора:
    	public delegate float Func(object funcArg);
    
    	public class HOFunctor
    	{
    
    		float _deltaTime = 0;
    		Func _func = null;
    		object _funcArg = null;
    		Guid _id = Guid.Empty;
    
    		public HOFunctor(float deltaTime, Func func, object funcArg)
    		{
    			_id = Guid.NewGuid();
    			_deltaTime = deltaTime;
    			_func = func;
    			_funcArg = funcArg;
    		}
    
    		public HOFunctor(float deltaTime, Func func) : this(deltaTime, func, null)
    		{
    		}
    
    		public bool Process(float deltaTime)
    		{
    			if (_func != null) 
    			{
    				_deltaTime -= deltaTime;
    				if (_deltaTime <= 0)
    				{
    					_deltaTime = _func(_funcArg);
    				}
    			}
    			return _deltaTime > 0;
    		}
    
    		public Guid ID
    		{
    			get
    			{
    				return _id;
    			}
    		}
    
    	}
    


    Functor Manager



    Фанктор менеджер — это класс, синглтон, хранящий список фанкторов (на самом деле 2 списка, об этом подробнее ниже).

    Итак, в менеджере есть 2 списка, один из которых активный на данный момент, а второй заполняется вновь добавленными фанкторами, в том числе и добавленными на момент исполнения активного списка. Так же есть переменная _currentIndex, она и определяется какой из списков сейчас активен.

    Большинство методов так же интуитивно понятны (AddFunctor(...) — добавляет фанктор в список, RemoveFunctor() — удаляет из списка). Еднственное, что стоит пояснения — это метод ProcessFunctors(float delta). Берётся текущий список, у каждого фанктора вызывается метод Process, чтобы он отсчитал время и если надо — выполнился, если фанктор вернул ненулевое значение (то есть время, через которое его надо вызвать повторно) — то он добавляется в новый список. В конце активный список очищается.

    Код менеджера:
    
    	public class HOFunctorMgr
    	{
    		#region Private fields
    
    		private static HOFunctorMgr _instance = null;
    		protected Dictionary<Guid, HOFunctor>[] _functors = {
    		   new Dictionary<Guid, HOFunctor>(),
    			 new Dictionary<Guid, HOFunctor>()
    		};
    		int _currentIndex = 0;
    
    		private HOFunctorMgr()
    		{
    
    		}
    
    		#endregion
    
    		public static HOFunctorMgr Instance
    		{
    			get
    			{
    				if (_instance == null)
    				{
    					_instance = new HOFunctorMgr();
    				}
    				return _instance;
    			}
    		}
    
    		#region Public methods
    
    		public Guid AddFunctor(float deltaTime, Func func, object funcArg)
    		{
    			return AddFunctor(new HOFunctor(deltaTime, func, funcArg));
    		}
    
    		public Guid AddFunctor(float deltaTime, Func func)
    		{
    			return AddFunctor(new HOFunctor(deltaTime, func, null));
    		}
    
    
    		public Guid AddFunctor(HOFunctor functor)
    		{
    			if (functor != null && !_functors[_currentIndex].ContainsKey(functor.ID))
    			{
    				_functors[_currentIndex].Add(functor.ID, functor);
    				return functor.ID;
    			}
    			return Guid.Empty;
    		}
    		
    		public void ProcessFunctors(float delta)
                    {
    			int indexToProcess = _currentIndex;
    			_currentIndex ^= 1;
    			foreach (HOFunctor f in _functors[indexToProcess].Values)
    			{
    				if (f.Process(delta))
    				{
    					AddFunctor(f);
    				}
    			}
    			_functors[indexToProcess].Clear();
    		}
    
            public void RemoveFunctor(Guid id)
            {
                if (_functors[0].ContainsKey(id))
                {
                    _functors[0].Remove(id);
                }
                if (_functors[1].ContainsKey(id))
                {
                    _functors[1].Remove(id);
                }
            }
    		#endregion
    
    	}
    


    Интеграция в проект



    Интеграция в проект производится 1 строкой. Нужно добавить вызов

    HOFunctorMgr.Instance.ProcessFunctors(delta);

    в игровом цикле, естественно предварительно добавив класс фанктор менеджера в проект. В моём случае (движок NeoAxis) это метод protected override void OnTick(float delta) игрового окна.

    delta — это время в секундах, прошедшее с предыдущего вызова.

    Всё, так просто.

    Примеры использования



    Однократный вызов:

    
      GameEntities.HOFunctorMgr.Instance.AddFunctor(2.0f, arg =>
      {
          HidePuzzleWindow();
          return 0.0f;
      },
      null );
    


    Через 2 секунды вызовется HidePuzzleWindow() и скроется окно с головоломкой.

    Многократный вызов:

    
      ...
      int k = 5;
      HOFunctorMgr.Instance.AddFunctor(5, arg =>
      {
          GameMap.Instance.AddScreenMessage(string.Format("{0}", arg));
          if (k-- >= 0) {
              return 5;
          }
          return 0;
      },
      "test");
    


    5 раз вызовется фанктор, выводящий на экран слово test, с интервалом в 5 секунд каждый раз. Интервал, кстате, можно менять.

    Вызов статичного метода:
    
      static float MyMethod(object arg)
      {
            if (arg != null)
            {
                ...
            }
    	return 0;
      }
    
      HOFunctorMgr.Instance.AddFunctor(10, MyMethod, someObject);
    


    Через 10 секунд вызовется статичный метод MyMethod и ему в качестве аргумента будет передан someObject.

    О использовании



    Вы можете модифицировать и/или использовать код в ваших проектах, как некоммерческих, так и коммерческих.

    Полный код класса



    Код класса, неймспейс заточен для NeoAxis-а.

    
    using System;
    using System.Collections.Generic;
    using System.Text;
    
    namespace GameEntities
    {
    
    	public delegate float Func(object funcArg);
    
    	public class HOFunctor
    	{
    
    		float _deltaTime = 0;
    		Func _func = null;
    		object _funcArg = null;
    		Guid _id = Guid.Empty;
    
    		public HOFunctor(float deltaTime, Func func, object funcArg)
    		{
    			_id = Guid.NewGuid();
    			_deltaTime = deltaTime;
    			_func = func;
    			_funcArg = funcArg;
    		}
    
    		public HOFunctor(float deltaTime, Func func) : this(deltaTime, func, null)
    		{
    		}
    
    		public bool Process(float deltaTime)
    		{
    			if (_func != null) 
    			{
    				_deltaTime -= deltaTime;
    				if (_deltaTime <= 0)
    				{
    					_deltaTime = _func(_funcArg);
    				}
    			}
    			return _deltaTime > 0;
    		}
    
    		public Guid ID
    		{
    			get
    			{
    				return _id;
    			}
    		}
    
    	}
    
    	public class HOFunctorMgr
    	{
    		#region Private fields
    
    		private static HOFunctorMgr _instance = null;
    		protected Dictionary<Guid, HOFunctor>[] _functors = {
    		   new Dictionary<Guid, HOFunctor>(),
    			 new Dictionary<Guid, HOFunctor>()
    		};
    		int _currentIndex = 0;
    
    		private HOFunctorMgr()
    		{
    
    		}
    
    		#endregion
    
    		public static HOFunctorMgr Instance
    		{
    			get
    			{
    				if (_instance == null)
    				{
    					_instance = new HOFunctorMgr();
    				}
    				return _instance;
    			}
    		}
    
    		#region Public methods
    
    		public Guid AddFunctor(float deltaTime, Func func, object funcArg)
    		{
    			return AddFunctor(new HOFunctor(deltaTime, func, funcArg));
    		}
    
    		public Guid AddFunctor(float deltaTime, Func func)
    		{
    			return AddFunctor(new HOFunctor(deltaTime, func, null));
    		}
    
    
    		public Guid AddFunctor(HOFunctor functor)
    		{
    			if (functor != null && !_functors[_currentIndex].ContainsKey(functor.ID))
    			{
    				_functors[_currentIndex].Add(functor.ID, functor);
    				return functor.ID;
    			}
    			return Guid.Empty;
    		}
    		
    		public void ProcessFunctors(float delta)
            {
    			int indexToProcess = _currentIndex;
    			_currentIndex ^= 1;
    			foreach (HOFunctor f in _functors[indexToProcess].Values)
    			{
    				if (f.Process(delta))
    				{
    					AddFunctor(f);
    				}
    			}
    			_functors[indexToProcess].Clear();
    		}
    
            public void RemoveFunctor(Guid id)
            {
                if (_functors[0].ContainsKey(id))
                {
                    _functors[0].Remove(id);
                }
                if (_functors[1].ContainsKey(id))
                {
                    _functors[1].Remove(id);
                }
            }
    		#endregion
    
    	}
    }
    


    Поделиться публикацией
    Похожие публикации
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама
    Комментарии 5
    • 0
      Функтор (а не фанктор) — это вообще-то совсем другое.

      ru.wikipedia.org/wiki/%D0%A4%D1%83%D0%BD%D0%BA%D1%82%D0%BE%D1%80_(%D0%BF%D1%80%D0%BE%D0%B3%D1%80%D0%B0%D0%BC%D0%BC%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5)
      • +2
        А сама идея — что-то среднее между примитивной банальностью и велосипедом.
        • 0
          В данной статье идёт речь о фанкторе (по правилам чтения английского языка читается фанктор), функтор как вы заметили действительно другое, хотя и созвучно.

          Это «tips and tricks», простые решения какой-то задачки, а не статья о каком-то сложном алгоритме.

          Ну и судя по тому, что статья даже не попала на главную и не вызвала интереса ни у кого, кроме 4-х человек — «tips and tricks» видимо на этом и закончится.
          • 0
            Мне все равно, как читается по правилам английского языка. Это слово давно заимствовано и перешло в русский язык и пишется как «функтор». Называть его «фанктором» — все равно, что называть Цукерберга Закебегом.
            Слово functor переводится как «функтор». А читается оно как «фанкта», если уж на то пошло.
        • +1

        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.