Открыть список
Как стать автором
Обновить

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

	}
}


Теги:gamedevfunctor managertips and tricks
Хабы: Разработка игр
Всего голосов 11: ↑8 и ↓3 +5
Просмотры5.8K

Комментарии 5

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

Похожие публикации

Серверный программист С++ (gamedev)
от 150 000 до 170 000 ₽2ReallifeМосква
Sales manager/Business Development Manager (IT разработка)
от 120 000 ₽SoftMediaLabЕкатеринбургМожно удаленно
Game Designer (Middle+)
до 150 000 ₽Rightway GamesМосква
Unity developer (middle)
от 80 000 до 100 000 ₽PiRL VenturesСанкт-ПетербургМожно удаленно
Middle С# разработчик (Unity / .NET)
от 80 000 до 120 000 ₽AdComboВоронежМожно удаленно

Лучшие публикации за сутки