TowerDefence #3. Первый враг

В основном все игры начинаются с обработки действий игрока в игре. Но в случае с товер дефенсом, я считаю, следует идти другим путем. Прежде чем добавлять башенки, которые будут отстреливать нечисть, добавим саму нечисть — так как это пожалуй самый сложный и интересный элемент.

При разработке игр мне лично больше всего нравится оживлять миры — добавлять какую-либо живность, триггеры, реагирующие на действия игрока или другие интересные конструкции. Пока всего этого в игре нет, мир кажется мертвым и грустным. А оживающий игровой мир зажигает огонек в глазах разработчика и подогревает энтузиазм. Так что сегодня мы добавим в игровой мир первые динамические объекты и их обработку.

Новые пакеты

Открываем наш проект и первым делом создаем две новые папки в com.towerdefence: первая папка будет enemies, а вторая towers. Так же аналогичные папки можно создать и в TowerDefence.fla в библиотеке клипов.

После того как папки созданы, открываем папку enemies и создаем в ней новый класс EnemyBase.as, унаследованный от класса Sprite — это будет наш базовый класс, от которого мы потом будем наследовать всех наших уникальных врагов. Создайте в нем три публичных пустых метода, которые будут общими для всех врагов:

// удаление врага
free():void { }

// инициализация
init():void { }

// обновление врага, здесь будут производиться все расчеты
update(delta:Number):void { }

Пока оставим все эти методы пустыми и вернемся к заголовку класса. Добавим пару публичных статических констант, которые будут отвечать за разновидность врагов, позже список можно будет дополнить.

public static const KIND_NONE:int = -1;
public static const KIND_SOLDER:int = 0;

Теперь добавим несколько внутренних переменных:

// ссылка на игровой мир
internal var _universe:Universe;

// разновидность врага
internal var _kind:int = KIND_NONE;

// здоровье врага
internal var _health:Number = 0;

// скорость движения врага по X
internal var _speedX:Number = 0;

// скорость движения врага по Y
internal var _speedY:Number = 0;

// визуальное представление врага
internal var _sprite:MovieClip;

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

Памятка для новичков: обратите внимание, что для переменных установлен модификатор internal. Модификатор internal означает, что переменная будет доступна, как локальная, в потомках класса. А модификатор private означает, что переменная доступна только внутри класса и нигде больше.

Как избежать прямых ссылок?

Кто только начал возиться с AS3 и написал хотя бы одну игру, я думаю уже успел столкнуться с тем, что в разные классы приходится передавать ссылки на основные классы в виде параметров, что в итоге негативно сказывается на читаемости кода и делает его запутанным.

В нашем случае мы уже создали переменную _universe, в которой будет храниться ссылка на игровой мир, чтобы враги могли взаимодействовать с ним. Например, обращаться к карте проходимости, а так же добавлять и удалять себя из игрового мира. ? первым, что напрашивается в голову — это, конечно, передать в конструктор врага ссылку на игровой мир.

Но есть более «красивый» способ, позволяющий избавиться от передачи прямых ссылок в игровые объекты на важные игровые классы. Суть способа такова, что при создании игрового мира мы сохраняем ссылку на его экземпляр внутри мира в приватную статическую переменную, и создаем для её чтения публичный метод (можно геттер), который позволит обратиться к экземпляру игрового мира из любого игрового класса, например так: Universe.getInstance().

Чтобы реализовать подобный функционал, давайте вернемся сейчас к классу Universe.as и добавим новую приватную статическую переменную:

private static var _instance:Universe;

После этого в конструкторе мира добавим пару строчек:

if (_instance != null)
{
  throw("Error: Мир уже существует. ?спользуйте Universe.getInstance();");
}
_instance = this;

Теперь создадим публичный статический метод getInstance(), который будет возвращать ссылку на единственный экземпляр игрового мира:

public static function getInstance():Universe
{
  return (_instance == null) ? new Universe() : _instance;
}

Если мир еще не существует, то он автоматически будет создан, в ином случае вернется ссылка на уже существующий мир.

Памятка для новичков: Модификатор static означает, что переменная может быть доступна в коде без создания и инициализации класса, частью которого она является.

А теперь давайте перейдем в класс EnemyBase.as и в его конструкторе напишем:

_universe = Universe.getInstance();

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

Когда ссылка на мир у нас известна, осталось доработать остальные методы класса EnemyBase.as следующим образом:

public function free():void
{
  if (_sprite != null && contains(_sprite))
    removeChild(_sprite);
  _universe.removeEnemy(this);
}

Метод free() — это стандартный метод разрушения всех объектов после того, как они больше не нужны в игре. В базовом методе мы удаляем спрайт из иерархии отображения и удаляем сам объект врага из игрового мира. Этот код будет общим для всех врагов.

Обычно в AS3 в качестве имени этого метода используют destroy, но я привык к free еще с тех пор, как писал в Delphi. Тем более free по написанию короче, чем destroy и более подходящий с той точки зрения, если объект нужно осовободить и положить, например к кэшированным объектам до тех пор пока он не понадобится вновь ;)

public function init():void
{
  if (_sprite != null)
    addChild(_sprite);
  _universe.addEnemy(this);
}

Метод init() — это наш стандартный метод инициализации врагов перед их использованием в игровом мире. Здесь мы добавляем спрайт в иерархию отображения и добавляем врага в игровой мир. Этот код так же будет общим для всех врагов.

public function get kind():int
{
    return _kind;
}

Геттер kind — возвращает разновидность врага. Пока не знаю зачем это может пригодиться, но если пригодится, то это будет использоваться так:

enemy = listOfEnemies[i];
switch (enemy.kind)
{
  case KIND_SOLDER :
    // какой-то код
  break;
  case KIND_TRANSPORT :
    // какой-то код
  break;
}

? это должно работать быстрее чем, например:

if (listOfEnemies[i] is SomeEnemy) 
{
  enemy = _listOfEnemies[i] as SomeEnemy; 
  // Какой-то код
}
else if (listOfEnemies[i] is FooEnemy) 
{
  enemy = _listOfEnemies[i] as FooEnemy; 
  // Какой-то код
}

На этом пока заканчиваем работу над базовым классом для всех врагов EnemyBase.as и приступаем к созданию первого врага.

EnemySolder.as

Следующим нашим шагом будет создание нового класса EnemySolder.as в папке enemies — это будет наш первый самый слабый враг. Данный класс мы должны унаследовать от EnemyBase.as и перекрыть основные методы базового класса, добавив уникальный функционал.

public override function init():void
{
  // Уникальные параметры врага
  _kind = EnemyBase.KIND_SOLDER;
  _health = 100;
  _speedX = 100;
  _speedY = 100;
        
  // Создание графического образа
  _sprite = new Solder_mc();
        
  super.init();
}

В методе init() мы задаем стандартные характеристики данного врага и создаем его графический образ, после чего вызываем метод init() родительского класса EnemyBase.as чтобы выполнить добавление врага в игровой мир.

Еще обратите внимание на 10 строку — здесь мы создаем графический образ злодея. То есть нужно добавить в библиотеку клипов в папку enemies новый клип, который будет визуализировать нашего врага и экспортировать его в ActionScript с именем Solder_mc.

public override function update(delta:Number):void
{
  // Временный код движения объекта
  // демонстрирующий работу класса
  x += _speedX * delta;
  y += _speedY * delta;
    
  // ?нвертируем скорость движения врага
  // если он достиг границ экрана       
  if (x >= App.SCREEN_WIDTH || x <= 0)
    _speedX *= -1;
  if (y >= App.SCREEN_HEIGHT || y <= 0)
    _speedY *= -1;
}

В методе update() мы добавляем код движения врага. В данный момент это простой код двигает врага в какую-нибудь сторону до тех пор, пока враг не достигнет границы экрана. Код в данный момент является временным, чтобы продемонстрировать работу врага.

Памятка для новичков: перекрыть метод — это значит переписать метод родителя в классе потомка каким-то новым функционалом. При этом, если метод перекрыт, то метод родителя не будет выполняться если мы не вызовем его явным способом, например: super.update() — где super это родительский класс. Для перекрытия родительских методов используется модификатор override в объявлении метода. Не перекрытые методы в классе потомке имеют родительский функционал.

На этом мы пока заканчиваем работу непосредственно над врагом и возвращаемся к доработке игрового мира.

Universe.as

?зменений уже сделано много и непременно хочется протестировать игру, но прежде чем компилировать игру, нужно еще доработать класс игрового мира и сделать тестовую инициализацию врагов.

Все враги, башни и пули внутри игрового мира у нас будут храниться в разных массивах, чтобы удобнее было с ними работать. Поэтому первым делом создадим три разных массива:

private var _listOfEnemies:Array = []; // Список врагов
private var _listOfTowers:Array = []; // Список башен
private var _listOfBullets:Array = []; // Список пуль

Но сегодня нам понадобится только один из них — _listOfEnemies. Если вы внимательно просматривали код в этой записи выше, то должны были заметить вызовы несуществующих методов _universe.addEnemy(this) и _universe.removeEnemy(this) в классе EnemyBase.as. Настало самое время написать эти два метода чтобы враги могли добавлять и удалять себя из игрового мира.

public function addEnemy(enemy:EnemyBase):void
{
  // Добавляем врага в список обрабатываемых объектов
  _listOfEnemies.push(enemy);
            
  // Добавляем врага на карту
  addChild(enemy);
}

public function removeEnemy(enemy:EnemyBase):void
{
  // Удаляем врага с карты
  if (contains(enemy))
    removeChild(enemy);
            
  // Удаляем врага из списка обрабатываемых объектов
  for (var i:int = 0; i < _listOfEnemies.length; i++)
  {
    if (_listOfEnemies[i] == enemy)
    {
      _listOfEnemies.splice(i, 1);
      break;
    }
  }
}

Эти два метода достаточно просты и я думаю, что от меня не требуется каких-либо дополнительных комметариев.

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

private var _deltaTime:Number = 0; // Текущее delta время
private var _lastTick:int = 0; // Последний тик таймера

В конструкторе игрового мира добавим слушателя на вход в кадр:

addEventListener(Event.ENTER_FRAME, enterFrameHandler);

? перейдем к написанию метода enterFrameHandler — это будет наш основной игровой обработчик, в котором будут производиться основные игровые расчеты игрового мира и всех игровых объектов.

private function enterFrameHandler(event:Event):void
{
  // Расчет delta времени
  _deltaTime = (getTimer() - _lastTick) / 1000;
            
  // Обработка врагов, находящихся в игровом мире
  for (var i:int = 0; i < _listOfEnemies.length; i++)
  {
    (_listOfEnemies[i] as EnemyBase).update(_deltaTime);
  }
            
  _lastTick = getTimer();
}

Пока обработчик выглядит достаточно просто и не должен вызывать особых вопросов. Но для новичков я хотел бы немного рассказать про _deltaTime.

Delta Time

В переменной _deltaTime хранится некий временной интервал между игровыми кадрами, которое мы используем для корректного расчета скорости движения игровых объектов. Нужен этот нехитрый трюк для того, чтобы объекты двигались всегда с положенной им скоростью, даже в том случае, если игра по какой-либо причине начнет тормозить. В случае если fps упадет, то при использовании delta времени объекты начнут двигаться рывками, а если задавать скорость движения игровых объектов без учета delta времени, то при падении fps игра будет выглядеть, как в замедленном режиме (как в желе). При программировании игр всегда рекомендуется рассчитывать интервал между кадрами и использовать его для расчета движений объектов, что позволяет повысить точность и привязать их движение к определенному временному интервалу, который не будет зависеть от скорости рендера в игре. Только вот во Flash не все так хорошо, как хотелось бы. getTimer() во Flash может давать погрешности, и тогда вся эта наука о точностях идет лесом, так как из-за этого могут возникать всякие неприятные глюки с дерганьем и скачками. Поэтому в некоторых случаях, работая во Flash, все же приходиться отказываться от динамического расчета deltaTime, используя например, одноразовый расчет при запуске игры:

_deltaTime = 1 / stage.frameRate.

Компилируем

Ну что, все готово к первому запуску. Осталось только добавить тестовое создание врага в конструкторе игрового мира и можно тестировать. Добавьте эти строки в конструкторе мира:

var solder:EnemySolder = new EnemySolder();
solder.init();

? не забудьте импортировать классы врагов в игровой мир строчкой:

import com.towerdefence.enemies.*;

Тестируем! Если все сделано правильно, то наш единственный враг должен перемещаться из правого верхнего угла по диагонали вправо и вниз до тех пор, пока не столкнется с краем экрана. Если что-то не работает или выводятся ошибки, сверьтесь с исходниками или еще раз перечитайте внимательно запись.

Домашнее задание

Сегодняшним домашним заданием будет задача добавить нового врага, унаследованного от EnemyBase. В качестве отличительной особенности задайте ему альтернативную скорость движения и новый графический образ. Не забудьте добавить нового тестового врага в игровой мир и посмотрите, что из этого получится.

Заключение

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

Ссылка на исходники — CS4, *.zip, 101 кб.

Содержание

  1. Вступление
  2. Структура игры
  3. Карта проходимости
  4. Первый враг
  5. Готовимся к поиску пути
  6. Поиск пути
  7. Редактор уровней
  8. Движение врагов
  9. Первая башня
  10. Кэширование объектов
  11. Полоса жизни
  12. Вражеские волны
  13. Загрузка вражеских волн
  14. Продолжение следует...

 

 

Классно! Как я вовремя зашел.
Здравствуй, бессонная ночь:)

Спасибо за урок, Антон. Я уже заждался.
Планируешь еще написать пару уроков до новогодних праздников или уже ждать их в 2011 году?)

Dyrk
3 Декабря 2010
— 01:10
#

не сочтите занудой - но "неч?сть"

Grammar Nazi
3 Декабря 2010
— 01:59
#

"Памятка для новичков: обратите внимание, что для переменных установлен модификатор internal. Модификатор internal означает, что переменна будет доступна, как локальная, в потомках класса. А модификатор private означает, что переменная доступна только внутри класса и нигде больше."

- тоже не совсем верно, то что вы описали это модификатор доступа protected

AS3 Nazi
3 Декабря 2010
— 02:01
#

@Dyrk, в идеале бы закончить всю серию уроков до нового года, но боюсь не успею. Планирую по два урока в неделю выдавать в течении декабря, так что до следующего года большая часть уроков возможно будет пройдена ;)

Ant.Karlov
3 Декабря 2010
— 03:56
#

Ну вот и продолжение. Спасибо.

Ваш красивый способ уже давно обрел название в известных паттернах программирования - это называется синглетон (Singleton).

shaman4d
3 Декабря 2010
— 04:15
#

@Grammar Nazi, спасибо, поправил.

@AS3 Nazi, все правильно написано. В своем описании я лишь забыл упомянуть, что при использовании модификатора internal переменные будут доступны потомкам только в рамках одного пакета. А при использовании protected переменные будут доступны потомкам в любом случае.

Ant.Karlov
3 Декабря 2010
— 04:22
#

@shaman4d, про наличие названия в паттернах для красивого способа я не знал. Зато теперь буду знать ;) Спасибо!

Ant.Karlov
3 Декабря 2010
— 04:26
#

@AS3 Nazi, все правильно написано

Не совсем всё-таки правильно.
internal не смотрит на потомков вообще.
т.е. эта переменная будет доступна всем классам пакета, независимо от того есть между ними какие-то взаимоотношения наследования или нет.

Dukobpa3
3 Декабря 2010
— 04:49
#

Ну а вообще - всё как всегда элегантно:) Читаю, просвещаюсь)))

Dukobpa3
3 Декабря 2010
— 04:51
#

Хорошо пишите. Не пишу на AS, но статьи всё равно читаю ))

shp
3 Декабря 2010
— 05:11
#

Антон интересная статья все просто и понятно.

?ван
3 Декабря 2010
— 07:30
#

Красивая и изящная структура игры. Многое почерпнул для себя. Сам до сих пор писал довольно кривую и спонтанную реализацию))
Антон, в классе врагов вы используете вложенный контейнер для графики. А почему просто не использовать привязку класса символу библиотеки?

Iktash
3 Декабря 2010
— 11:14
#

Приятно встретить людей, которые не знают о красивых названиях вроде "паттерн Singleton" и научном трактовании модификатора internal, но умеющих писать логичный, структурированный и красивый код.

А то от грамотеев в чьём коде вжизнь не разберёшься - уже передёргивает. :)

Алексей
3 Декабря 2010
— 11:28
#

Не уверен, что к месту, но скажу. Стоит где-то отметить, что помимо Array можно создавать переменные типа Vector для тех, кто ориентируется на 10 плеер. Т.к. тауэр дефенсы прожорливы в плане ресурсов, их использование было бы целесообразней.

adzh
3 Декабря 2010
— 11:33
#

?звините, парочку очепяток: "сложный и инЕтересный элемент" в заголовке и наверное Вы имели в виду "Soldier" - (англ) солдат, потому что "solder" это совсем другая штука...
Ещё не рекоммендовал бы использовать такой код:

if (x >= App.SCREEN_WIDTH || x <= 0)
_speedX *= -1;

Сам когда то на таком обжигался, при проседании FPS юнит может начать топтаться на одном месте в течении некоторого времени постоянно интвертируя скорость туда-обратно. Лучше разбивать на два if`а, конкретно указывая знак для скорости, тогда бага не будет...

hanuman
3 Декабря 2010
— 11:36
#

В методе update() класса EnemySoldier я бы немного изменил условия инвертирования скорости на такие:

// ?нвертируем скорость движения врага
// если он достиг границ экрана
if (x + width / 2 >= App.SCREEN_WIDTH)
{
x = App.SCREEN_WIDTH - width / 2;
_speedX *= -1;
}
else if (x <= width / 2)
{
_speedX *= -1;
x = width / 2;
}

if (y + height / 2 >= App.SCREEN_HEIGHT)
{
y = App.SCREEN_HEIGHT - height / 2;
_speedY *= -1;
}
else if (y <= height / 2)
{
y = height / 2;
_speedY *= -1;
}
а то при существующих условиях враг выходит за границы экрана.

Zaphod
3 Декабря 2010
— 11:39
#

мдааа развелось грамотеев в комментах ))))
лю тя тоха! твой вечный друг)))
ПС: я не робот, я робокоп))))))

ant tananuka
3 Декабря 2010
— 13:05
#

Мдяааа....повар варит суп. ? все знают что он умеет варить впринципе....но нет жешь, после слов повара "А теперь мы скиним марковку в кастрюлю....", - почти всегда найдется тот, кто крикнит: "А посолить то забыл!!!!".

Ребят, вы думаете Антон не умеет делать игры?!

з.ы. Сегодня 3Deкабря, кто в теме с праздником!....а так же на сегодня был намечен релиз. Где он?!

з.ы.ы. а я терминатыыыырррррр...

NoICE
3 Декабря 2010
— 16:18
#

Спасибо, всегда приятно читать!

Несколько коментариев:

// Удаляем врага из списка обрабатываемых объектов
for (var i:int = 0; i < _listOfEnemy.length; i++)

1 - Лучше не использовать _listOfEnemy.length в цикле, на каждой итерации будет происходить расчет значения.

2. Вместо цикла можно использовать _listOfEnemy.indexOf(enemy), так как надо найти один элемент.

? как писали раньше вектор быстрее массива, но для старта массив проще ;)

Можно еще подробнее о "getTimer() во Flash может давать погрешности"?

Flasher
3 Декабря 2010
— 17:24
#

Мне понравилась статья, все толково написано, спасибо Антон! Все жду информацию про прелоадер, интересно как его лучше делать.

А насчет проседания FPS и синхронизации по deltaTime. Это скорее панацея от небольших отклонений FPS, в случае очень низких значений этой величины объекты могут просто улететь в бесконечность (например когда флешка только запустилась и FPS еще не просчитан правильно).
Как вариант, можно использовать фиксированную частоту обновления и стараться компенсировать падение FPS дополнительными, "догоняющими" витками цикла обновления. Тогда замедление объектов будет не так заметно и они не будут терять стабильность.

BuxomBerry
3 Декабря 2010
— 17:52
#

Mining Truck 2 уже вышел?

Zamzim
3 Декабря 2010
— 18:30
#

Всё здорово. Не использую дельту, не стал делать переменную для юниверс внутри объектов. Мне проще напрямую через Universe - так я сразу вижу где что лежит. По скорости не знаю насколько медленнее. Векторы тоже не использую потому что FlashCS3. length всегда использую. Под всех енемей у меня единственный класс base_enemy. Я тоже планировал наследовать и расширять, но со временем оказалось что это ненужная вещь. Почему? Ну наверное потому что этих врагов переваливает за 30 штук, а механика поведения ничем не отличается. А вот под башни сделал. Для движений использовал готовый класс поиска путей по связанным нодам (А* но не через ячейки). Спецом пришлось редактор делать, чтобы можно было шустро расставить и связать. У тебя вроде будет классический А*, но если вдруг нужны будут эти ноды обращайся. По поводу скорости всего этого дела я не загоняюсь, вроде не ракету делаю. Главное чтобы тормозов особых не было.

Еще придется хранить довольно дофига статистики по всей игре - как планируешь делать? Сам пользую XML + кое что в печеньках. Хранить всё это дело тупо внутри классов (ручками там записывать) выходит довольно громоздко и неудобно. Кароче настоятельно рекомендую добавить урок про редактор всего и вся! Начиная с тех же енемей.

RaymondGames
3 Декабря 2010
— 18:56
#

Антон, скажи пожалуйста ты нормально относишься к критике/советам относящимся к этим урокам? Некоторые авторы комментариев, по-видимому нет. Если тебя критика каким-то образом задевает - скажи, ты все-таки делаешь хорошее дело и не хотелось показаться неблагодарным.

Спасибо.

AS3 Nazi
3 Декабря 2010
— 18:58
#

@Dukobpa3, да, ты прав. Посмотрел в книжке и в ?нтернет — вспомнил. Ну, некоторые «научные трактования» могут забываться со временем если долго не задумываться об этом :) В целом сути дела это особо не меняет.

Ant.Karlov
3 Декабря 2010
— 19:28
#

@Iktash, это у меня такая не очень хорошая привычка делать вложенные спрайты для визаулизации объектов. Появилась она непосредственно во флеше благодоря его уникальной особенности вложения одного клипа в другой и получении на выходе одного клипа состоящего из кучи других клипов :)

Кстати в MT2 такой подход оказался очень удобным для навешивания различных трофеев на машинку.

Ant.Karlov
3 Декабря 2010
— 19:37
#

@Алексей, поскольку я не «чистый программист» и помимо программирования приходится много возится с графикой и анимацией, то просвещение по всем фронтам получается не достаточно углубленным как хотелось бы. Спасибо!

Ant.Karlov
3 Декабря 2010
— 19:41
#

@adzh, это все же просто урок и некоторые решения не достаточно оптимальны, зато понятна работа алгоритма ;) Все желающие углубится в тему, могут на свое усмотрение оптимизировать код сколько угодно.

Ant.Karlov
3 Декабря 2010
— 19:47
#

@hanuman, код движения врага в данном уроке является «однаразовым» и служит только для того чтобы показать работу метода update(). Поэтому я не стал писать сложных условий чтобы небыло лишних вопросов.

Опечаток действительно хватает, поправлю. Спасибо!

Ant.Karlov
3 Декабря 2010
— 19:50
#

@Zaphod, как только мы сделаем алгоритм поиска пути и сможем получать список клеток по которым враг должен перемещаться, то текущий код из метода update() можно будет удалить и забыть о нем. Поэтому я не стал тратить время на написание сложных рассчетов для корректного отскакивания врага и тем более объяснять как все эти условия должны работать ;)

Спасибо, что вы написали это! Думаю кому-нибудь это будет интересно.

Ant.Karlov
3 Декабря 2010
— 19:55
#

@NoICE, :D !!

Присоеденяюсь к поздравлению 3Dэшников!

Ant.Karlov
3 Декабря 2010
— 19:59
#

@Flasher,

1. А я ведь еще думал, стоит ли в рамках урока вынести значение length в отдельную переменную. Но, потом решил не пугать новичков лишними строчками кода :) В любом случае, в циклах, которые выполняются не каждый кадр (например в removeEnemy();) перерасчет значения length не критичен.

2. Я по привычке использую «дедовские методы» работы с массивами, так как в тех языках программирования, с которыми я работал ранее, не было таких удобных штук, как indexOf().

По поводу getTimer() у меня получился немного двусмысленный текст. Сам getTimer() достаточно точный, большие погрешности могут возникать, когда флешка может неожиданно начать тормозить по независящим от неё причинам. Все знают, что спонсоры монетезируют свои порталы и наши игры за счет рекламы на своих порталах и в большинстве случаев рекламы на порталах в избытке и большая её часть так же является флешовыми баннерами. ? если баннеро-дел имел кривые ручки и не осторожность, например, динамически создавать околок 20-30 партиклов с разными модификациями для создания эффекта взрыва внутри баннера, то все... пиши пропало :) Кстати, реальный случай, когда баннер на одном из порталов подвешивал почти все игры. В общем в игру с динамическим расчетом delta в таком случае сложно уже будет поиграть чем, например, с фиксированной delta.

Ant.Karlov
3 Декабря 2010
— 20:48
#

@BuxomBerry, про deltaTime согласен, как раз об этом достаточно подробно написал в предыдущем комментарии. В общем в большинстве случае все-таки наиболее подходящее решение это именно фиксированное deltaTime.

Ant.Karlov
3 Декабря 2010
— 20:50
#

@Zamzim, нет еще не вышел. По России и СНГ 4-го будет релиз, так как в америке будет еще 3 декабря :) Об этом я напишу отдельно.

Ant.Karlov
3 Декабря 2010
— 20:53
#

@RaymondGames,

1. deltaTime можно использовать по желанию. Можно хоть каждый кадр двигать объекты по пиксельно без учета какой-либо delta ;)

2. Вызов метода внешнего класса должен работать медленнее чем обращение к внутренней переменной (я не тестировал). Так что если происходит регулярное обращение, например к игровому миру в рамках какого-нибудь цикла, то лучше ссылку на него получать один раз и запоминать её в локальной переменной. Ну и строки кода будут короче и более читаемые ;)

3. С новыми векторами которые могут помочь оптмизировать работу со списками я еще особо не знаком, поскольку это вроде как новая фича и работает не во всех плеерах. Потому пока по старинке работаю с Array.

4. ? все же стоит выносить обращение к array.length за приделы цикла, если речь идет о циклах которые выполняются, например каждый кадр — т.к. это все лишние вычисления.

5. В формате данного урока тоже можно было бы сделать один класс для всех типов врагов. Но хотелось бы раскрыть в уроке чуть больше и показать как можно удобно наследовать классы расширяя их функционал оставляя их маленькими и аккуратными. Я попробую придумать что-нибудь уникальное, что позволит сделать мое разделение на подклассы оправданными.

6. У меня есть в голове один совсем примитивный поиск пути и чуть более сложный с более правильным подходом поиска — оба алгоритма на базе «волны» расходящейся в стороны. ? я думаю, что это то, что нужно в рамках урока. Но ноды это тоже интересно, спасибо! :)

7. Конечно про редактор будет и про тюнинг средствами xml тоже все будет, просто не добрались пока еще до этого :) Капельку терпения!

Ant.Karlov
3 Декабря 2010
— 21:11
#

@AS3 Nazi, к критике отношусь нормально. Критика бывает правильная, а бывает «не правильная» поэтому приходится фильтровать. В плане кода и алгоритмов я не считаю себя идеальным и истиной в последней инстанции ;) ? в некоторых комментариях бывают действительно ценные замечания которые подталкивают задуматься и пересмотреть некоторые свои вещи — это интересно и развивает :P

Ant.Karlov
3 Декабря 2010
— 21:15
#

@Zamzim
http://www.mochigames.com/game/mining-truck-2-trolley-transport/

Тут играется

P0lter
3 Декабря 2010
— 21:55
#

Ant.Karlov - очень хорошие tutorials.

Хочу обратить внимание на одну опасность использования паттерна singleton. Да, казалось бы очень удобно и хорошо, что не нужно передавать указатели на universe. Но представь на секунду, что ты захочешь показать в окошке тьюториал на самом игровом движке, или сделать парное прохождение (т.е. у тебя будет два _universe). В этом случае придется отовсюду аккуратно выпалывать все ссылки на статический объект. Все-таки лучше делать ссылки объектов друг на друга в явном виде. Есть много приемов, как в этом случае их автоматически и удобно подставлять. Есть даже целые framework для этого (http://www.spicefactory.org/parsley/) - правда для небольшой игры проще написать свою небольшую фабрику.

Сергей Батищев
4 Декабря 2010
— 10:08
#

Спасибо за статью, как раз сегодня для меня - 100% в тему.
Успехов!

torboot
4 Декабря 2010
— 13:10
#

а я всеравно тебя люблю!
Галка! Я не робот! Я робокоп))))))))))))

Тёска)
4 Декабря 2010
— 23:55
#

@Сергей Батищев, показывать туториал на движке или делать парное прохождение (кстати, что это? Сплитскрин на двоих или демонстрация предыдущего повтора одновременно с текущим?) — достаточно сложные фичи о которых следует тщательно подумать заранее, так как для их грамотной реализации влюбом случае нужно будет делать соответствующую архитектуру игры.

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

В общем, мне лично еще ни разу не понадобилось наличие второго экземпляра игрового мира или еще что-то в таком духе. Поэтому я стараюсь использовать этот самый «синглтон» для основных игровых классов :)

Ant.Karlov
5 Декабря 2010
— 03:23
#

Ant.Karlov - я просто хотел отметить фундаментальную (и не всем всегда заметную) проблему этого паттерна. Фактически он разрушает возможность соединять разработанные компоненты по необходимости, вместо явных вводит скрытые и неявные зависимости (так, например, разработанную пушку нельзя использовать в другой не tower defense игре с другим universe), очень затрудняет написание unit tests (и создает впечатление, что unit test - это не для игр).

Сила объектного дизайна в том, что даже достаточно сложные фичи часто не нужно продумывать заранее. Если архитектура продумана правильно, компоненты имеют low coupling/high cohersion, то легко сочетать ранее разработанные компоненты для любых новых задач.

Это не значит, что синглетоны использовать нельзя. Это значит, что их нужно использовать понимая, какое зло они несут и какую пользу они несут, и принимая взвешенное решение.

Сергей Батищев
5 Декабря 2010
— 09:29
#

Хотя в принципе, я внимательнее прочитал, как ты это все делаешь: _universe = Universe.getInstance(); - это очень грамотно. Когда будет нужно, будет легко в одном месте перейти с синглетона на более "правильную" технику. А до той поры можно наслаждаться простотой синглетонной реализации. ;)

Сергей Батищев
5 Декабря 2010
— 09:52
#

Отличный урок! Очень рад, что эти уроки есть, помогают учится и разбираться в as3.

Мне кажется немного не красиво писать условиях таким образом:
if (условие)
//код
Думаю, всё же лучше не забывать о фигурных скобках, даже если всего и одна строчка.

Кстати, выполнил всё что в уроке, исправил все ошибки из-за своей внимательности и при компиляции у меня теперь АутПат выдает такое "Error #1023: Переполнение стека.", что это значит и как от этого избаиться?

J0x
5 Декабря 2010
— 12:46
#

_universe = Universe.getInstance();

вот из-за этой строчки у меня и происходит "Переполнение стека", которая находится в EnemyBase.as

J0x
5 Декабря 2010
— 17:14
#

@J0x, рекомендации форматирования кода позволяют использовать условия без фигурных скобок с некоторыми оговорками.

По поводу переполнения стека, рекомендую сверится с исходником. Скорее всего из-за невнимательности еще что-то может быть сделано не так. В первую очередь проверь наличе строки _instance = this; в конструкторе игрового мира Universe.as.

Ant.Karlov
6 Декабря 2010
— 00:52
#

Ура, дождались таки! Чувствую, сегодня меня ожидает интересный вечери наедине с AS3. :)

jarofed
6 Декабря 2010
— 14:06
#

Скачал исходник, понял в чём ошибка моя была.

____________________
if (_instance != null)
{
throw("Error: Мир уже существует.");
}
_instance = this;
______________________
Данный код надо было в самое начало конструктора пропистаь, а у меня он был в серёдке конструктора. Не знал о такой строгости)

J0x
11 Декабря 2010
— 08:46
#

В классе EnemyBase на строке 42: super();
У меня заработало и без него, но интересно для чего эта функция вызывается.

Al
12 Декабря 2010
— 19:35
#

Отличный урок!

symfony
12 Декабря 2010
— 21:43
#

@Al,

super() — это вызов конструктора родительского класса (в данном случае вызов конструктора Sprite). Это не обязательный вызов. У меня шаблон так настроен, что при создании класса данный вызов уже указан.

Ant.Karlov
12 Декабря 2010
— 23:09
#

Здраствуйте, а как можно реализовать deltaTime (то есть независимую от fps скорость движения объектов) в играх, использующих Box2D (как, например, MT2)?

Макс Д
25 Декабря 2010
— 15:45
#

Спасибо, за урок!
В тексте есть небольшая опечатка:
Список врагов объявляется как _listOfEnemies, а используется как _listOfEnemy.

Тшлшгы
23 Февраля 2011
— 10:29
#

Никакой очепятки нету, просто в уроке вроде не было сказанно о добавлении нового массива private var _listOfEnemy:Array = [];
Агромное спасибо за такие уроки, автору низкий поклон.....

Mr.Ocher
5 Марта 2011
— 12:41
#

А нет, действительно опечатка.. )), просто подумал что, нужно еще добавить массив, а он уже был, просто под другим именем.

Mr.Ocher
5 Марта 2011
— 12:46
#

@Макс Д, фиксированный deltaTime — это один раз его посчитать и всегда использовать:

_deltaTime = 1 / 40;

зы.: Что-то я позднов-то ваш комментарий увидел :(

Ant.Karlov
5 Марта 2011
— 23:00
#

@Тшлшгы и @Mr.Ocher, спасибо за наводку! Ошибки поправил.

Ant.Karlov
5 Марта 2011
— 23:01
#

Как сделать нескольких врагов?

Roman
14 Марта 2011
— 22:35
#

@Roman, для создания нескольких врагов достаточно создавать нового врага через определенный интервал времени (например, вызывать метод newEnemy();).

Ant.Karlov
15 Марта 2011
— 11:54
#

как избавится от отладочной сетки? она сверху и из-за нее не видно врага, только когда он ударяется о стенку

Алекс
22 Марта 2011
— 14:22
#

Получается вы используете таймер для всех движущихся объектов и программных анимаций?

pavezlo
11 Апреля 2011
— 23:12
#

@pavezlo, нет в данном случае таймер не используется. При использовании динамического deltaTime привязка движений осуществляется к реальному времени, а не к количеству кадров. Но если сделать deltaTime статическим, то время уже будет зависеть от количества кадров в секунду.

Ant.Karlov
12 Апреля 2011
— 10:00
#

У меня ошибка возникает
TypeError: Error #1006: init не является функцией.
at Universe()[C:UsersuDesktopTD(по уроку)Universe.as:37]
at Game()[C:UsersuDesktopTD(по уроку)Game.as:12]
at App()[C:UsersuDesktopTD(по уроку)App.as:18]

Почему?

? еще вопрос: все ваши уроки я понимаю процентов на 40%, хотя прочитал справочник Адоб и Колина Мука. Что еще посоветуете почитать для полного понимания?

Randy
29 Апреля 2011
— 21:42
#

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

Randy
30 Апреля 2011
— 11:44
#

кода*

Randy
30 Апреля 2011
— 11:45
#

@Randy, от порядка кода все очень сильно зависит. При разработке нужно быть внимательным к тому в какой последовательности все создается и используется.

Для полного понимания еще нужен опыт который приходит только с практикой.

Ant.Karlov
30 Апреля 2011
— 12:12
#

Спасибо!
Благодаря вам наконец-то хоть немного уловила, зачем нужен getInstance(), виденный мною во многих программах!
Наконец-то я им пользоваться научилась.
Стыдно. =)

Demy
23 Августа 2011
— 14:33
#

отличные уроки, но меня интересовала больше структура проектов. спасибо.

noname
8 Сентября 2011
— 03:19
#

SOLDIER... SOLDIER!

soldier - солдат
solder - припой, спайка...

noname
8 Сентября 2011
— 11:45
#

Что посоветуете при возникновении подобной ошибки при выполнении этого урока

Error: Error #1023: Переполнение стека.
at flash.display::DisplayObject()
at flash.display::InteractiveObject()
at flash.display::DisplayObjectContainer()
at flash.display::Sprite()
at myGame::Universe()
at myGame::Universe$/getInstance()
at myGame.danger::EnemyBase()
at myGame.danger::EnemyAlien()
at myGame::Universe()
at myGame::Universe$/getInstance()
at myGame.danger::EnemyBase()
at myGame.danger::EnemyAlien()
at myGame::Universe()
at myGame::Universe$/getInstance()
at myGame.danger::EnemyBase()
at myGame.danger::EnemyAlien()
at myGame::Universe()
at myGame::Universe$/getInstance()
at myGame.danger::EnemyBase()
at myGame.danger::EnemyAlien()
at myGame::Universe()
at myGame::Universe$/getInstance()
at myGame.danger::EnemyBase()
at myGame.danger::EnemyAlien()
at myGame::Universe()
at myGame::Universe$/getInstance()
at myGame.danger::EnemyBase()
at myGame.danger::EnemyAlien()
at myGame::Universe()
at myGame::Universe$/getInstance()
at myGame.danger::EnemyBase()
at myGame.danger::EnemyAlien()
at myGame::Universe()
at myGame::Universe$/getInstance()
at myGame.danger::EnemyBase()
at myGame.danger::EnemyAlien()
at myGame::Universe()
at myGame::Universe$/getInstance()
at myGame.danger::EnemyBase()
at myGame.danger::EnemyAlien()
at myGame::Universe()
at myGame::Universe$/getInstance()
at myGame.danger::EnemyBase()
at myGame.danger::EnemyAlien()
at myGame::Universe()
at myGame::Universe$/getInstance()
at myGame.danger::EnemyBase()
at myGame.danger::EnemyAlien()
at myGame::Universe()
at myGame::Universe$/getInstance()
at myGame.danger::EnemyBase()
at myGame.danger::EnemyAlien()
at myGame::Universe()
at myGame::Universe$/getInstance()
at myGame.danger::EnemyBase()
at myGame.danger::EnemyAlien()
at myGame::Universe()
at myGame::Universe$/getInstance()
at myGame.danger::EnemyBase()
at myGame.danger::EnemyAlien()
at myGame::Universe()
at myGame::Universe$/getInstance()
at myGame.danger::EnemyBase()
at myGame.danger::EnemyAlien()

выполнял во FlashDevelop просто вдруг это его особенность и мне куда то в другое место за советом обратится?

karasev
24 Октября 2011
— 20:02
#

В EnemySolder.as вылетают две ошибки компиляции

1119: Access of possibly undefined property SCREEN_WIDTH through a reference with static type Class.

и вторая такая же только про высоту экрана.

так как я делаю сам, по тексту статьи и только сверяюсь с исходниками, то это вызвало у меня недоумение.

Посмотрев в код App.as я увидел, что у нас стоит
public static const SCR_W:int = 640;
public static const SCR_H:int = 480;

в общем то ли я тупанул, то ли определение этих констант было поменяно в ходе написания статей и автор забыл предупредить )

Возможно я ошибаюсь.
В общем, всем на заметку если кто то столкнется с подобной проблемой.

Hudson
22 Декабря 2011
— 16:03
#

Спасибо за урок! крайне познавательно, после Мука самое то.

Такая же проблема как у Hudson, только чуть подругому:

строка:
if (x >= App.SCR_W || x <= 0)

1120: Access of undefined property App.

Да, в уроке неправильные константы указаны...но не в этом суть. Не могу понять, почему из EnemySolder вообще нет доступа к App... ?

Прописал пока вручную 640 480 - всё работает =)

Zoich
14 Марта 2012
— 02:19
#

@Zoich, @Hudson, где-то в комментариях к урокам меня раскритиковали по поводу сокращения имен констант/переменных даже для очевидных значений, я их изменил и забыл предупредить об этом.

Класс App не доступен скорее всего потому что нет его импорта в тот класс где вы его используете. Нужно добавить строку:

import com.towerdefence.App;

Ant.Karlov
14 Марта 2012
— 03:19
#

@Ant.Karlov, и действительно. Я почему-то считал что этот Solder - дальний родственник App..

Ещё, в уроке нигде не упоминается
import flash.utils.getTimer;

хотя, может и к лучшему, копипастить будут меньше)

Обратная связь радует =)

Zoich
14 Марта 2012
— 14:12
#

Не понял такой вещи: для чего в классах EnemyBase и EnemySoldier делается import com.towerdefence.Universe?

Для каких целей этот "обратный" импорт, если в самом Universe уже сделан импорт классов врагов?

Kingo
11 Апреля 2012
— 08:57
#

Это вопрос больше из области ООП.
Enemybase не знает о том кто его импортировал.
То что Universe является обладателем объектов порожденных от Enemybase ничего не значит для самих объектов. Они не знают когда были созданы, до того как инициализировался Universe или уже после этого. ?х мог создать совсем другой класс, а потом передать в Universe.
Короче говоря – классам необходимо знать об Universe (точнее об указателе на объект _universe а не на сам класс Universe) для того чтоб добавлять себя в мир, удалять себя оттуда и взаимодействовать с этим миром.
Более детально о взаимодействии классов стоит почитать, например, в книге Колина Мука.

FirstFlashGame
11 Апреля 2012
— 12:13
#

Здрасте, подскажите пожалуйста о чом говорит эта строка
_listOfEnemies[i] as EnemyBase).update(_deltaTime)

и не совсем понятно в этойже функции в параметре написано event:Event тоже не доконца понял.

Alexsandr
11 Апреля 2012
— 14:21
#

// Обработка врагов, находящихся в игровом мире
for (var i:int = 0; i < _listOfEnemies.length; i++)
{
(_listOfEnemies[i] as EnemyBase).update(_deltaTime);
}
_listOfEnemies – это массив. В него кладем экземпляры потомков EnemyBase. При доступе к элементу массива мы должны уточнить какой именно тип объекта туда положили.
_listOfEnemies[i] as EnemyBase означает что компилятору необходимо трактовать i-овый элемент массива как экземпляр класса EnemyBase.
Все это делается для того, чтоб пробежаться по всем элементам массива (это наши враги), и вызвать каждому представителю класса EnemyBase функцию update(). Таким образом, каждый враг сохраненный в массиве _listOfEnemies получит возможность двигаться по карте.

FirstFlashGame
11 Апреля 2012
— 15:47
#

FirstFlashGame, спасибо за разъяснение.
Что-то подобное я и предполагал )
Прочитал тут ещё в умной книжке более простое объяснение - если классы относятся к разным пакетам, то их нужно взаимно импортировать.

Kingo
12 Апреля 2012
— 15:40
#

(_listOfEnemies[i] as EnemyBase).update(_deltaTime);

выдает ошибку Error: Мир уже существует. ?спользуйте Universe.getInstance();

При авто формате заменяется на:
_listOfEnemy[i] as EnemyBase.update(_deltaTime);

и ошибка выглядит так:

1061: Call to a possibly undefined method update through a reference with static type Class.

Rev
18 Сентября 2012
— 15:15
#

1) _listOfEnemy[i] as EnemyBase.update(_deltaTime);
Вызываем статический метод update() класса EnemyBase, а результат работы ф-ии должен трактоваться как специфический класс для элемента _listOfEnemy. Думаю это совсем не то, что планировалось сделать ;) Поэтому оставляем старый вариант (_listOfEnemies[i] as EnemyBase).update(_deltaTime);

2) Ошибка: Error: Мир уже существует. ?спользуйте Universe.getInstance();
Является результатом того, что где-то в коде ф-ии Update есть вызов конструктора Universe. Например, что-то из разряда ... new Universe.
Самый простой способ - это воспользоваться отладчиком и посмотреть по стеку в каком именно месте возникла ошибка.

FirstFlashGame
18 Сентября 2012
— 22:14
#

Спасибо!

были лишние строчки
_universe = new Universe();
addChild(_universe);
в App.as

Rev
19 Сентября 2012
— 14:52
#

подскажите что такое delta, из хелпа написано "Получает размер изменения в положение прокрутки в пикселях.Положительное значение указывает, что направление прокрутки был вниз или вправо.Отрицательное значение указывает, что направление прокрутки была вверх или влево."
наверно это е совсем то так как переводил через гугл переводчик, а хотелось бы узнат зачем он нужен в данном случае

vetalos
7 Марта 2013
— 23:56
#

@vetalos, вообще под delta подразумевается какой-либо интервал между чем либо (шаг). В хелпе вы наткнулись на описание свойства delta для работы с колесом мыши в котором хранится интервал между текущим положением колеса мыши и предыдущим — этот интервал используется для расчета количества пространства которое будет прокручно.

В нашем же случае под delta скрывается временной интервал между текущим и предыдущим кадром. ?спользуя время (delta) и скорость (velocity) мы вычисляем какое расстояние преодолел наш юнит за время прошедшее между кадрами. Такой подход считается наиболее корректным так как скорость будет всегда одинаковой независимо от количества кадров (fps).

Ant.Karlov
11 Марта 2013
— 15:37
#

Откомпилировалось нормально, но при запуске выдаёт несчётное количество раз:
Error #1063: Несоответствие количества аргументов в ru.towerdefence::World/enterFrameHandler().
Ожидалось 0, получено 1.
Что делать?

Alb
21 Мая 2013
— 13:30
#

@Alb, проблема в том, что ваш метод ожидает 0 аргументов, а получает 1. Все обработчики событий как правило получают один аргумент в котором передается само событие.

То есть в вашем случае строчку:

function enterFrameHandler():void

надо заменить на:

function enterFrameHandler(event:Event):void

и проблема решится.

Ant.Karlov
22 Мая 2013
— 10:58
#

>Памятка для новичков: обратите внимание, что для переменных установлен модификатор internal. Модификатор internal означает, что переменная будет доступна, как локальная, в потомках класса. А модификатор private означает, что переменная доступна только внутри класса и нигде больше.<
public везде.
internal только внутри пакета.
protected подклассам.
private только в классе.
Вы же написали:
>Модификатор internal означает, что переменная будет доступна, как локальная, в потомках класса<.

SuriTheAngel
15 Декабря 2013
— 21:09
#

?ли я вас не так понял, или вы напутали с потомками.

SuriTheAngel
15 Декабря 2013
— 21:11
#

@SuriTheAngel, internal — это доступ любым классам в пределах одного пакета. В памятке я ошибся.

Ant.Karlov
15 Декабря 2013
— 22:04
#

Вроде я все правильно сделал, по уроку.
В окне "враг" у меня иногда при ударе о верхний край или углы начинает дрыгаться вместо движения дальше на полсекунды или секунду. Не при каждом ударе, но через 3-4 удара о верх.

SuriTheAngel
20 Декабря 2013
— 19:24
#

Оказывается, не только о верхний край. "Застревает" в стенках на полсекунды или секунду. Так и должно быть?

SuriTheAngel
20 Декабря 2013
— 19:28
#

Я догадался, почему так происходит. Когда объект выходит за пределы дальше, чем нужно, скорость умножается на -1, и он застревает, опять умножается на -1, и так несколько раз, пока не выскочит из стены. Ранее где-то читал статьи по геймдеву, там они делали так:

if(x <= 0)
{
xSpeed *= -1;
x = 0;
}
else if(x >= SCREEN_W)
{
xSpeed *= -1;
x = SCREEN_W;
}
else if(y <= 0)
{
ySpeed *= -1;
y = 0;
}
else if(y >= SCREEN_H)
{
ySpeed *= -1;
y = SCREEN_H;
}

SuriTheAngel
20 Декабря 2013
— 19:35
#

Он мне всё время выдаёт, что тип EnemyBase не является константой времени компиляции или не найден. При этом в именах классов опечаток я не допустил и в Universe импортировал содержимое com.towerdefence.enemies. В адресах нигде не ошибся...( Что характерно, на EnemySolder он не ругается.

Алексей
26 Апреля 2014
— 01:50
#

Здравствуйте. У меня проблема: F:..Universe.as, строка 170 1046: Тип не найден или не является константой времени компиляции: EnemyBase.

Виктор
2 Января 2015
— 22:06
#

Угу , сколько то годков прошло !

zackie
29 Ноября 2015
— 17:12
#