четверг, 22 сентября 2011 г.

Подсветка синтаксиса

Как я делаю подсветку синтаксиса в своём блоге?
Очень просто, я просто копирую код из VisualStudio и вставляю его в Word.
А потом копирую его из Word и вставляю в редактор блоггера.
Подсветка сохраняется, ура!

Как мне показалось не все браузеры одинаково хорошо ладят с таким подходом, на всякий случай уточню, что у меня браузер Google Chrome, Word 2010, Windows XP Sp3, Visual Studio 2010.

воскресенье, 4 сентября 2011 г.

Nano Stream (2009)


История

     При разработке игры наступает такой момент, когда игра почти готова, и уже можно играть. Главное не обмануться этой мыслью, ведь основная часть айсберга всегда скрыта под водой. Я пока не довёл ни одной игры до состояния истинной готовности, но есть три игры, которые я довёл до состояния видимой готовности.
     Первой моей недовершённой игрой был тетрис, который я написал почти на спор, на Blitz3D. Спор заключался в том, чтобы написать тетрис и уложиться в 20 строк по 80 символов (точно не помню сколько), написал я его за вечер, потому не включаю  в счёт серьёзных проектов.
      Второй недовершённой игрой был Tower Defense  Nano Stream”, которому и посвящена данная статья.
     Первого декабря 2009г на сайте blitzmax.ru стартовал конкурс игр на тему Tower Defense. Я не стал участвовать, т.к. в моём понимании было, что Tower Defense  - это нечто жестокое, когда куча пушек палит в живых существ, от которых во все стороны разлетается кровь. Я не мог представить не жестокий TD. Точнее мог, но он казался мне невообразимо скучным. Я писал: ”Пример по конвейеру едут ёлки и нужно успевать наряжать их игрушками. Или по проводу текут частицы и надо гасить их анти частицами. Даже если игроку этот сюжет покажется интересным, то мне как девелоперу неинтересно работать с этим. Я не люблю такую натянутость“.  В итоге развязалась дискуссия на тему, возможно ли создать интересный не жестокий TD, благодаря которой присоединился к конкурсу с проектом с глупым названием Nano Stream. Целью данного проекта было доказать всем и самому себе, что невозможно создать интересный не жестокий Tower Defense с глупым сеттингом. А сеттинг был такой: по прозрачной трубе двигаются цветные наночастицы (шарики), задача игрока строить турели которые бы нейтрализовывали наночастицы до того как частицы доберутся до другого конца трубы.
     Конкурс проходил во время зимней сессии, точно также как “Конкурс №1” на сайте GameDev.by в котором я участвовал год спустя с проектом Ice&Flora. Если вы будете организовывать конкурс, то советую не делать этого во время сессии.
     На GameDev.by до финиша дошло только трое из 25-ти участников. Стоит ли говорить, что на blitzmax.ru я единственный из конкурсантов, кто представил работу для подведения итогов. Впрочем, я так и не смог закончить этот проект потому, что случайно удалил исходники, когда форматировал диск. 


     Я считаю, проект удался на славу, и из него есть что подчерпнуть. Далее подробнее о тонкостях игры:

Движение шариков

     Шарики движутся по трубе, которая диаметром чуть больше чем диаметр шарика. При этом шарики разных цветов движутся с разной скоростью. Одна из ключевых идей игры в том, как шарики обгоняют друг друга.
     Когда один шарик догоняет другой, то задний шарик упирается в передний и снижает скорость до скорости переднего. Далее они так и едут вместе паровозиком. Но реальная скорость заднего шарика выше. Разница скоростей способствует накоплению виртуального пути пройденного задним быстрым шариком.  Когда накопленная разница в пути достаточно большая для того чтобы обогнать  передний шарик и держаться впереди него то они быстро обмениваются местами проходя сквозь друг друга.
     Во-первых нужно заметить, что передний шарик также может иметь накопленный путь, возможно, он также упёрся в более медленный шарик. Этот момент учтён при проверке возможности обгона. Во-вторых, во время обгона шарик, перемещающийся назад, теряет пройденный путь, т.е. ему после этого справедливости ради нужно присвоить виртуальный пройденный путь.
     Когда случается так, что виртуальный путь накоплен, а обгонять некого (например, когда шарик, которого пытались обогнать был нейтрализован) то накопленный путь расходуется в виде небольшой временной прибавки к скорости шарика. 
     Получается очень справедливо и красиво.

Трёхмерность

     Честно, я никогда не видел трёхмерный Tower Defense. Особо не искал, но всё же. Не так-то просто придумать стратегию в 3D. Nano Stream - относится как раз к таким редким стратегиям, где действительно используются все три измерения, хоть и не очень эффективно.
     Трёхмерность заключается, конечно, не в графике, а в игровой механике: Труба изгибается во всех направлениях; турели имеют трёхмерную область действия, и даже слепые пятна сверху и снизу; За деньги у турели можно улучшить угол поворота по вертикали тем самым уменьшив слепые зоны; За деньги можно увеличить высоту турели тем самым немного сместив область действия; и наконец, турели можно строить не только на земле, но и на стенах и на потолках (цена строительства турели на стене на 10% выше, а на потолке на 50% выше).
     Создать именно трёхмерный TD было одним из моих челенджей на тот момент.
Шарики
     Всего в игре семь цветов шариков, они обладают разными свойствами: иммунитетами к отдельным видам турелей,  разная скорость, разное “здоровье”. Жёлтый шарик обладает особым свойством увеличивать скорость по мере того как остаётся мало здоровья.
     К тому же шарики бывают полупрозрачные (т.е. те же самые шарики могут быть обычными, а могут быть полупрозрачными). Полупрозрачные шарики называются невидимыми. Чтобы стрелять по ним нужно за деньги покупать для турелей специальную способность “Super Vision”.

Турели и абсорберы

     В игре 4 вида турелей и 4 вида абсорберов.
     Обычная турель – плазменная, поджигающая турель – термальная (подожжённые шарики постепенно нейтрализуются) , замораживающая турель – криогенная (замороженные шарики движутся медленнее), и лазерная - L.A.S.E.R. set.
     L.A.S.E.R. set – это огромная турель которая при стрельбе следит за шаром постоянно выжигая его лазерным лучом. Причём лазер разогревается медленно, т.е. чем дольше лазерный луч выжигает шарик, тем сильнее он его нейтрализует, когда лазерный луч погасает, турель быстро остывает. Угловая скорость турели медленная, поэтому он может терять быстрые цели, что плохо т.к. для полной отдачи лазер должен как можно дольше выжигать цель. Но за деньги можно улучшить угловую скорость, к тому же можно использовать лазерную турель совместно с замораживающей.

     Абсорберы (поглотители) – особый вид строений. – Это такие колодцы, которые вытягивают шарик из трубы и проглатывают (поглощают) его целиком, несмотря на то, сколько у него было “здоровья”. Абсорберы очень долго перезаряжаются и поглощают только шары определённого цвета. Собственно 4 вида абсорберов означают абсорберы 4х ключевых цветов: красный, тёмно синий, фиолетовый, жёлтый. Абсорберу можно купить способность Utilization, которая даёт один дополнительный нанобакс за каждый поглощённый шар.

Экономика

     Когда-то давно играл в игру Rise of Legends и подцепил оттуда одну интересную идею. Делать каждый следующий юнит данного вида дороже. Т.е.  каждая следующая термальная турель стоит дороже, поэтому как бы игрок не старался ему придётся строить и другие турели т.к. термальные станут слишком дорогими, если он начнёт строить криогенные то и с ними произойдёт тоже, ему придётся перейти на плазменные и т.д. Таким образом, у нас, разработчиков, меньше проблем с балансом.

Тактика

     Обычно в играх TowerDefense тактика заключается в грамотной расстановке турелей и грамотном их апгрейде. А по кому будет стрелять турель, от игрока не зависит. Я же реализовал возможность выбирать поведение для каждой турели.

     Выписка из руководства:

     Существует 4 основных критерия, по которым турель выбирает себе цель среди шаров.
     1) На первом месте соблюдается приоритет по цвету. При помощи специальных кнопок можно выставить приоритет по цвету для любой турели кроме поглотителей.
     2) Во вторых соблюдается приоритет по видимости. При помощи специальных кнопок можно установить какого типа видимости шары будут приоритетно выбираться в качестве цели. (Видимые, Без разницы, Невидимые)
     3) Третий критерий — это эффект. Огненная пушка накладывает на шары эффект поджигания, а ледяная эффект заморозки. Если на шаре уже есть эффект то на него нельзя подействовать эффектом другого типа(но можно подействовать эффектом того-же типа тем самым усилив эффект и/или добавив времени) Поэтому существует приоритет на эффект.
     Данная опция для ледяной пушка означает что пушка будет стараться заморозить как можно больше шаров. т.е. будет стараться не стрелять по уже замороженным шарам, подожжённым шарам (ведь их не заморозить) и синим шарам(у них иммунитет к заморозке). Однако эту опцию можно и отключить.
     Для огненной пушки данная опция означает что пушка будет стараться поджечь как можно больше т.е. она будет стараться не стрелять по уже подожжённым шарам, по замороженным шарам (ведь их не поджечь) а также по жёлтым шарам(иммунитет). Эту опцию можно отключить.
     Для Лазерной турели и бластера тоже есть специальная тактика на эффекты: Эта опция означает, что турель будет стараться не стрелять по подожжённым шарам и позволят им спокойно догорать. Для чего это нужно думайте сами.
     4) Четвёртый критерий — это положение. Этот критерий гласит, что при прочих равных условиях выбирается шар, идущий впереди других (впереди) Эту тактику настраивать нельзя (пока) она едина для всех.
     Все эти опции не запрещают стрельбу по тем или иным шарам, а лишь расставляют приоритеты.

Попробуйте сами

     Вы можете свободно скачать и сыграть в эту игру. Правда кроме прочих недостатков в игре имеется недостаток баланса. На уровне сложности 2 игра непроходима, а на уровне сложности 1 – слишком лёгкая. Я рекомендую вам зайти в настройки и заменить уровень сложности на 1. К тому же я рекомендую прочитать файл Help.pdf перед игрой.




суббота, 30 июля 2011 г.

Pool vs FastCollection. Возможные проблемы.

Данная статья является продолжением статьи: http://hale32bit.blogspot.com/2011/07/blog-post.html


Pool vs FastCollection

Все коллекции, представленные в предыдущей статье, могут использоваться вместо пула. Возникает вопрос: в каких случаях всё же лучше использовать пул? Какие есть преимущества Fast Collection перед Pool? Я вижу всего два преимущества:

1) Возможность обхода всех активных объектов
2) Защита от дублирования элементов

Первое преимущество можно выкинуть т.к. очевидно, что если нам нужен обход элементов, то мы выберем FastCollection, а не Pool т.к. в Pool его просто нет.
Защита от дублирования элементов означает, что мы можем дважды возвратить в Pool один и тот же объект, что в конечном итоге может привести к тяжёлым последствиям, в FastCollection невозможно удалить один объект дважды. Pool, на мой взгляд, очень опасный паттерн, ошибки которого довольно трудно отлавливать, а FastCollection чуть более надёжен. Эту тему я продолжу чуть позже, а пока рассмотрим преимущества паттерна Pool.

1) Немного быстрее
2) Использует меньше памяти
3) Не требует наследования объектов от специального класса

Как можно заметить Pool немного быстрее удаляет объекты т.к. при этом не происходит рокировка и перезапись индексов. К тому же Pool не требует памяти под хранение индексов элементов. Поэтому, несмотря на всю опасность, нам всё ещё хочется использовать Pool. Какие ошибки возникают при работе с пулом и как их можно обнаружить? Далее я расскажу свой взгляд на эту проблему.

Возможные ошибки

1) Утечка мусора.
Может так случиться, что по окончанию использования объекта программа (т.е. программист) забудет вернуть его в пул, а вместо  этого просто обнулит ссылки. Тогда им займётся сборщик мусора. Если мы используем FastCollection, тогда ссылка на объект никогда не будет потеряна и получится утечка памяти.

2) Дублирование элементов.
Если вернуть в пул один и тот же объект два раза, тогда он потенциально может быть выдан два раза, получится полный кавардак. В FastCollection такое невозможно т.к. каждому объекту соответствует уникальный индекс в массиве.

3) Призраки.
Если программа возвратит объект в пул, но при этом продолжит его использовать – это, в конечном счёте, может привести к утечке мусора или дублированию элементов в пуле. С FastCollection – та же проблема.

4) Элементы без прописки.
Это самая безобидная проблема. Просто если программист забудет, что есть пул и будет создавать объекты напрямую с помощью конструктора, то возможно он забудет их и вернуть в пул, что приведёт к утечке мусора. Хотя он может не забыть их вернуть в пул – что приведёт к нерациональному использованию памяти, т.к. созданием объектов занимается не пул, который старается инстацировать по возможности меньше объектов. При использовании FastCollection элементы без прописки довольно опасны, однако далее будет показано, как можно изменить код, чтобы контролировать ситуацию.     

Утечка мусора и дублирование элементов.

Обе проблемы опасны для пула, и трудно отловить баг в момент, когда он происходит. Однажды в конце игры, или в момент, когда один игровой уровень выгружен, а следующий ещё не загружен, все объекты должны быть возвращены в пул, назовём это нормальным состоянием пула.  Я предлагаю проверять нормальное состояние пула, действительно ли оно нормальное. Так мы можем узнать, произошло ли в течение игры дублирование элементов или утечка мусора.
Анализ заключается в том чтобы сравнить число элементов пуле с числом уникальных элементов в пуле – таким образом, будет обнаружено наличие дубликатов. Но и это ещё не всё. В нормальном состоянии все объекты должны быть возвращены в пул. Поэтому если сравнить полное число инстацированых элементов и число уникальных элементов в пуле можно узнать была ли утечка мусора.  Конечно, для этого необходимо вести подсчёт инстацированых объектов. Далее приведён код, которым я пользуюсь и который мне очень помог:

    public interface IPoolable { }


    /// <summary>
    /// Базовый класс для пула
    /// </summary>
    public class PoolBase
    {
        //Список всех пулов в приложении
        static protected readonly List<PoolBase> _pools = new List<PoolBase>(10);

        /// <summary>
        /// Анализ всех пулов
        /// </summary>
        [Conditional("DEBUG")]
        public static void AnalyzePools()
        {
            foreach (PoolBase pool in _pools)
                pool.Analyze();
            _pools.Clear();
        }

        [Conditional("DEBUG")]
        protected virtual void Analyze()
        {
        }
            
    }


    public class Pool<T> : PoolBase
        where T : class, IPoolable
    {

        Func<T> _instancer;
        T[] _elements;
        int _released = 0;
        int _instanced = 0;



        public int Count
        {
            get
            {
                return _elements.Length;
            }
        }

        public Pool(Func<T> instancer, int capacity = 1000)
        {
            _instancer = instancer;
            _elements = new T[capacity];
           
            #if DEBUG
            _pools.Add(this);
            #endif

        }

        private T RemoveObject()
        {
            if (_released > 0)
            {
                T obj =  _elements[_released - 1];
                _released--;
                if (obj != null)
                    return obj;
            }
            return null;
        }

        private T CreateObject()
        {
            T newObject = _instancer();
            _instanced++;

            //
            if (_instanced + _released > _elements.Length)
                throw new ApplicationException("Pool overflow");

            return newObject;
        }


        public T GetObject()
        {
            T thisObject = RemoveObject();
            if (thisObject != null)
                return thisObject;
            return CreateObject();
        }

        public void Release(T obj)
        {
            //
            if (obj == null)
                throw new NullReferenceException();

            //
           
             if(_released == _elements.Length)
                throw new ApplicationException("Pool overflow");
             else    
                _elements[_released] = obj;

            _released++;
        }


        /// <summary>
        /// Вспомогательный иттератор
        /// </summary>
        /// <returns></returns>
        private IEnumerable<T> ReleasedElements()
        {
            for (int i = 0; i < _released; i++)
                yield return _elements[i];
            yield break;
        }


        protected override void Analyze()
        {

            //Подсчёт уникальных объектов
            int unique = ReleasedElements().Distinct().Count();

            //Проверка нормального состояния пула
            if (unique != _instanced || unique != _released)
            {
                throw new ApplicationException("Несоответствие инстацированных, возвращённых и уникальных объектов пула "
                    + this.ToString() + "\n" +
                    "Инстацировано : " + _instanced.ToString() + "\n" +
                    "Возвращено : " + _released.ToString() + "\n" +
                    "Уникальных : " + unique.ToString()
                    );
            }
        }

    }

     Чтобы проверить нормальное состояние достаточно вызвать метод PoolBase.AnalyzPools() по окончанию игры.
    К сожалению, не представляю, как можно было бы обнаружить утечку памяти при использовании FastCollection. Может с помощью слабых ссылок?

Призраки

     Призраков пожалуй невозможно отловить не в случае пула не в случае FastCollection.

Элементы без прописки

     Чтобы в других частях программы объекты не создавались помимо пула, можно сделать закрытый конструктор:


    public class Class1 : IPoolable
    {

        private Class1() { }


        static Pool<Class1> _pool = new Pool<Class1>(() => new Class1());


        static Class1 InstantFromPool()
        {
            return _pool.GetObject();
        }

        public void ReleaseToPool()
        {
            _pool.Release(this);
        }

    }

     В случае FastCollection можно по умолчанию выдавать новым объектам отрицательный индекс, при этом исключение будет возникать каждый раз, когда мы пытаемся удалить из коллекции элемент не существующий в ней.
     В отличие от пула может оказаться нужным создать несколько FastCollection одного типа. Например, две FastCollection спрайтов, спрайты заднего плана, и спрайты переднего плана. Чтобы можно было сначала рисовать спрайты заднего плана, а потом переднего, иначе если бы они были в одной FastCollection, то перемешались бы. Как защитится от удаления не из той коллекции, и как реализовать перекидывание элемента из одной коллекции в другую. Можно модифицировать FastCollection как показано ниже, все изменения отмечены комментариями.

    public abstract class FastCollectionElement
    {
        int _fastCollectionIndex = -1;
        internal int FastCollectionIndex
        {
            get
            {
                return _fastCollectionIndex;
            }
        }
        internal void SetFastCollectionIndex(int index)
        {
            _fastCollectionIndex = index;
        }
    }


    public class FastCollection<T> : IEnumerable<T>
        where T : FastCollectionElement, new()
    {
        T[] _elements;
        int _top = -1;
        int _absoluteTop = -1; //Верхняя граница неиспользуемых элементов
        int _enumerators = 0;

        public FastCollection(int capacity)
        {
            _elements = new T[capacity];
        }

        public T GetNewElement()
        {
            //
            if (_enumerators > 0)
                throw new ApplicationException();

            //
            if (_top == _elements.Length - 1)
                throw new ApplicationException();

            //
            _top++;
            if (_elements[_top] == null)
            {
                _elements[_top] = new T();
                _elements[_top].SetFastCollectionIndex(_top);

                _absoluteTop++; //Увеличение верхней границы неиспользуемых элементов
            }


            return _elements[_top];
        }


        public void Clear()
        {
            //
            if (_enumerators > 0)
                throw new ApplicationException();

            //
            _top = -1;
        }


        public void Remove(T element)
        {
            //
            int index = element.FastCollectionIndex;

            //
            if (index > _top)
                throw new ApplicationException();

            //Проверка на принадлежность к коллекции
            if (!Contains(element))
                throw new ApplicationException();

            //
            if (_enumerators > 0)
                throw new ApplicationException();

            //
            if (index < _top)
            {
                //
                T temp = _elements[_top];
                _elements[_top] = _elements[index];
                _elements[index] = temp;

                //
                _elements[_top].SetFastCollectionIndex(_top);
                _elements[index].SetFastCollectionIndex(index);

            }


            //
            _top--;
        }

        /// <summary>
        /// Проверка на принадлежность объекта коллекции O(1)
        /// </summary>
        public bool Contains(T element)
        {
            return _elements[element.FastCollectionIndex] == element;
        }

        /// <summary>
        /// Извлечение элемента из коллекции
        /// </summary>
        public void Pop(T element)
        {
            int index = element.FastCollectionIndex;

            //Проверка на принадлежность
            if (!Contains(element))
                throw new ApplicationException();

            //Проверка на используемость
            if (index > _top)
                throw new ApplicationException();

            //Специальная рокировка
            _elements[index] = _elements[_top];
            _elements[_top] = _elements[_absoluteTop];
            _top--;
            _absoluteTop--;
            _elements[index].SetFastCollectionIndex(index);

            //Пометка что элемент не используется ни в какой коллекции
            element.SetFastCollectionIndex(-1);
        }


        /// <summary>
        /// Вставка в коллекцию элемента, ранее не пренадлежавшего ей
        /// </summary>
        public void Push(T element)
        {
            //Проверка на принадлежность элемента какой-либо коллекции
            if (element.FastCollectionIndex != -1)
                throw new ApplicationException();

            //Проверка на переполнение
            if (_absoluteTop == _elements.Length - 1)
                throw new ApplicationException();


            //увеличение границы
            _top++;
            _absoluteTop++;

            //специальная рокировка
            _elements[_absoluteTop] = _elements[_top];
            _elements[_top] = element;
            element.SetFastCollectionIndex(_top);

        }


        IEnumerator<T> IEnumerable<T>.GetEnumerator()
        {
            return new FastCollectionEnumerator(this);
        }

        IEnumerator IEnumerable.GetEnumerator()
        {
            return new FastCollectionEnumerator(this);
        }

        public class FastCollectionEnumerator : IEnumerator<T>
        {
            FastCollection<T> _collection;
            int _curIndex = -1;

            public FastCollectionEnumerator(FastCollection<T> collection)
            {
                _collection = collection;
                _collection._enumerators++;
            }

            public T Current
            {
                get
                {
                    return _collection._elements[_curIndex];
                }
            }

            object IEnumerator.Current
            {
                get
                {
                    return _collection._elements[_curIndex];
                }
            }

            public bool MoveNext()
            {
                _curIndex++;
                return (_curIndex <= _collection._top);
            }

            public void Reset()
            {
                throw new NotImplementedException();
            }

            public void Dispose()
            {
                _collection._enumerators--;
            }

        }

    }

     Вот такая вот FastCollection, хотя уже не такая быстрая, но более гибкая. Кстати этот код я ещё не испытывал, он может содержать ошибки.