Альтернатива «Cache as bitmap» — разгружаем процессор

Наверное многие знают и умело используют такую замечательную возможность во флеше как «Cache as bitmap«. При этом, с клипа делается виртуальный снимок в виде картинки и вектор не пересчитывается для отрисовки каждый кадр. Можно рисовать графику в векторе прямо во флеше, потом в панели свойств включить кэширование и на выходе плеер получает растровую картинку. Очень удобно, можно быстро вносить изменения и сразу любоваться результатом минуя фазу экспорта/импорта в PNG. Плюс ко всему — экономия траффика.

Звучит приятно и обычно всё хорошо работает, но я столкнулся с тем, что с клипами больших размеров, тормоза частично остаются. В одной платформенной игре у меня был фон 1200х1200, который лежал на заднем плане и должен был просто смещаться, пока персонаж бегает по уровню. Так вот, анимация происходила с небольшими рывками. Такое впечатление, что флэш время от времени пересчитывает данные и делает «обновлённый» снимок с клипа. Так или иначе, но факт имел место и такие тормоза были. Это решалось заменой векторного фона растровым (PNG), но такая флешка весила очень много.

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

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

[kml_flashembed movie="http://xitri.com/wp-content/uploads/2009/03/rasterizeimage.swf" height="200" width="550" /]

Читать далее Альтернатива «Cache as bitmap» — разгружаем процессор

Crimsonland 3: Выживание на практике

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

На самом деле о некоторых вещах я уже писал раньше. Например, монстры будут появляться таким же способом, как препятствия в баннере с мячиком (помните?). Для простоты изложения мы будем использовать процедурный способ программирования с разделением кода на слои :). Потом так будет удобнее ориентироваться в коде (это первый шаг к классам и ООП).

[kml_flashembed movie="http://xitri.com/wp-content/uploads/2009/03/crimsongame.swf" height="200" width="550" /]

Итак, из предыдущих постов у нас есть главный герой, готовый ринуться на толпы врагов. Я заменил базуку на пулемёт и немного повысил его манёвренность (скорость ходьбы и поворота). Слайд 1.

Подробно о том, как теперь устроен клип body клипа hero показано на слайте 2. При щелчке мышью, начинает проигрываться анимация стрельбы и во втором кадре вызывается функция placeBullet(), которая добавляет пулю на сцену. Это единственное место в этом примере, где вызов функции идёт из клипа, а не с главной линейки. Обратите внимание, что появился вспомогательный клип dot — он нужен чтобы пересчитать по нему правильные координаты пули в момент вылета из ствола. Это идеальный способ, когда дуло не находится точно на осях X и Y. Внутри всех вспомогательных клипов я пишу _visible = false. Так они не заметны для глаза, но доступны для кода.

Дальше создаём клип для противника с именем spider и прочую атрибутику: пулю (bullet) и спецэффект для поражения (boom). Клип spider состоит из 2-х кадров (состояний). В первом клип с анимацией ходьбы (здесь же и клип hit, который нужен для проверки попадания), а во втором — клип а анимацией погибания (когда анимация доигрывается, в последнем кадре вызывается _parent.removeMovieClip() которая удаляет противника (в данном случае _parent — это клип spider). Клип пули bullet — просто рисунок снаряда (при _rotation = 0 смотрит вправо). Клип boom — имитация кровавого всплеска. Все эти клипы присутствуют на линейке, но лежат далеко за пределами сцены чтобы случайно не попали в кадр (или выполнить для них _visible = false). Потом с каждого будет создаваться дубликат и использоваться по необходимости.
Создаём прямоугольный клип ground который площадью должен покрыть всю сцену. Противники будут появляться, учитывая его границы, а пули будут удаляться (в смысле delete), когда вылетят за его пределы. Слайд 3.

Следующее что мы сделаем — создадим выше 4 слоя для скрипта. На самом верхнем, первом слое, будет выполняться главный цикл (onEnterframe), который будет перемещать героя и добавлять противников. На втором — всё что касается движения главного героя. На третьем и четвёртом всё для пуль (выстрелов) и монстров соответственно (слайд 4).

Дальше привожу код по слоям сверху вниз:

  1. Основной цикл.

    Каждый 50-й кадр идёт вызов функции placeSpider(), которая добавляет на сцену противника (она описана дальше)

    //переменная таймера для добавления противников на сцену
    var spiderCount = 0;
    //хранит уровень куда добавляются объекты
    var lev = 0;
    
    onEnterFrame = function(){
      //функция действия главного героя
    	actionHero();
    
    	if(spiderCount++ > 50) {
    		spiderCount = 0;
    		//добавляем паучка
    		placeSpider();
    	}
    }
  2. Главный герой.

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

    var step = 3;
    var rotDirSpeed = 6;
    var rotToMouseSpeed = 7;
    
    function actionHero () {
    	if (Key.isDown(Key.LEFT)) {
    		hero._rotation -= rotDirSpeed;
    	} else if (Key.isDown(Key.RIGHT)) {
    		hero._rotation += rotDirSpeed;
    	}
    
    	var mDx = _xmouse - hero._x;
    	var mDy = _ymouse - hero._y;
    	//угол поворота между клипом hero и мышкой в градусах
    	var mAngleD = Math.atan2(mDy, mDx) / Math.PI * 180;
    	//угол поворота между башней и мышкой в градусах
    	var dAngleD = hero._rotation + hero.body._rotation - mAngleD;
    
    	//без этой проверки башня будет неправильно крутиться
    	//при переходе границы -180 и +180 градусов
    	if (dAngleD > 180) {
    		dAngleD = -360 + dAngleD;
    	} else if (dAngleD < -180) {
    		dAngleD = 360 + dAngleD;
    	}
    	//поворачиваем башню с нашей скоростью
    	if(Math.abs(dAngleD) < rotToMouseSpeed) {
    		hero.body._rotation -= dAngleD;
    	} else if(dAngleD > 0) {
    		hero.body._rotation -= rotToMouseSpeed;
    	} else {
    		hero.body._rotation += rotToMouseSpeed;
    	}
    	//проверка на реализм поворота корпуса человека
    	if(hero.body._rotation < -90) {
    		hero.body._rotation = -90;
    	} else if(hero.body._rotation > 90) {
    		hero.body._rotation = 90;
    	}
    
    	var dirAngle = hero._rotation / 180 * Math.PI;
    	if (Key.isDown(Key.UP)) {
    		hero.foot.play();
    		hero._x += step * Math.cos(dirAngle);
    		hero._y += step * Math.sin(dirAngle);
    	} else if (Key.isDown(Key.DOWN)) {
    		hero.foot.play();
    		hero._x -= step * Math.cos(dirAngle);
    		hero._y -= step * Math.sin(dirAngle);
    	} else {
    		hero.foot.stop();
    	}
    
    	//стреляем если зажата клавиша мышки
    	if(Key.isDown(1)){
    		hero.body.play();
    	}
    };
    
    //скрываем эталонный клип, все дубликаты при этом будут видимы
    blood._visible = false;
    
    //дублирует спецэффект и располагает поверх героя
    placeBlood = function() {
    	lev++;
    	var d = blood.duplicateMovieClip("b" + lev, lev + 20000);
    	d._x = hero._x;
    	d._y = hero._y;
    }
  3. Пули.

    Задаётся скорость полёта, функция которая добавляет пулю на сцену и задаёт сценарий действий. Проверка на попадание осыществляется старым проверенным способом — через функцию hitTest(). Перебираем всех пауков что есть на сцене и проверяем пересечение. Это выполняется для каждой летящей пули каждый кадр. Лучше не проверять главные клипы противников на пересечение, а создать внутри них вспомогательные клипы с заданной формой, которые будут не видны (скрыты _visible = false), но при этом будут учавствовать в расчётах (так можно сделать попадание только в тело, без учёта ног).

    //скорость полёта пули
    bulletSpeed = 18;
    
    //функция добавляет пулю на сцену, перемещает к дулу пулемёта,
    //поворачивает по направлению выстрела и прописывает для неё алгоритм движения
    placeBullet = function (tgt) {
    	lev++;
    
    	//пересчитываем координаты конца дула в координаты текущего клипа
    	var p = {x:tgt._x, y:tgt._y};
    	tgt._parent.localToGlobal(p);
    	globalToLocal(p);
    
    	//создаём дубликат клипа с пулей, позиционируем и поворачиваем
    	var d = bullet.duplicateMovieClip("b" + lev, lev);
    	d._x = p.x;
    	d._y = p.y;
    	d._rotation = hero._rotation + hero.body._rotation;
    	d.a = d._rotation / 180 * Math.PI;
    
    	//алгоритм действий пули
    	d.onEnterFrame = function() {
    		//смещение
    		this._x += bulletSpeed * Math.cos(this.a);
    		this._y += bulletSpeed * Math.sin(this.a);
    
    		//проверка на выход за границы сцены
    		if (this._x < ground._x - this._width || this._x > ground._x + ground._width + this._width || this._y < ground._y - this._height || this._y > ground._y + ground._height + this._height) {
    			this.removeMovieClip();
    		}
    
    		//проверка попадания в противников
    		//ВНИМАНИЕ: в коде проверяется пересечение клипа текущей пули с ВНУТРЕННИМ клипом hit текущего противника
    		var i = spiders.length;
    		while (i--) {
    			var curS = spiders[i];
    			if (curS.hit.hitTest(this)) {
    				this.removeMovieClip();
    				curS.gotoAndStop(2);
    				removeSpider(curS);
    			}
    		}
    	};
    
    };
  4. Враги.

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

    //массив хранит список всех противников которые бегают по сцене в данный момент
    var spiders = new Array();
    
    //добавляет паука на сцены и задаёт для него поведение
    function placeSpider() {
    	lev++;
    	var d = spider.duplicateMovieClip("z" + lev, lev);
    
    	//случайным образом выбираем с какой стороны экрана ему появиться
    	if (Math.random() < .5) {
    		d._x = ground._x + Math.random() * ground._width;
    		if (Math.random() < .5) {
    			d._y = ground._y - d._height / 2;
    		} else {
    			d._y = ground._y + ground._height + d._height / 2;
    		}
    	} else {
    		d._y = ground._y + Math.random() * ground._height;
    		if (Math.random() < .5) {
    			d._x = ground._x - d._width / 2;
    		} else {
    			d._x = ground._x + ground._width + d._width / 2;
    		}
    	}
    
    	//первоначальный поворот на цель и определение скорости
    	var dx = hero._x - d._x;
    	var dy = hero._y - d._y;
    	d.a = Math.atan2(dy, dx);
    	d._rotation = d.a / Math.PI * 180;
    	d.speed = Math.random() * 2 + 2;
    
    	//поведение противника
    	d.onEnterFrame = function() {
    		//смещается туда куда смотрит
    		this._x += this.speed * Math.cos(this.a);
    		this._y += this.speed * Math.sin(this.a);
    
    		//постоянно корректирует своё направление движения на главного героя
    		var dx = hero._x - this._x;
    		var dy = hero._y - this._y;
    		this.a = Math.atan2(dy, dx);
    		this._rotation = this.a / Math.PI * 180;
    
    		//проверка на достижение цели
    		var dist = Math.sqrt(dx * dx + dy * dy);
    		if (dist < 25 && !placed) {
    			//запустить спецэффект
    			placeBlood();
    			//запустить клип с сообщением о проигрыше
    			//в этом примере не описан, но можете иметь ввиду
    			_parent.black.play();
    			delete this.onEnterFrame;
    		}
    	};
    	//добавляем новосозданного паука в список
    	spiders.push(d);
    }
    
    //функция удаления паука из списка противников
    //вызывается при попадании пули
    function removeSpider(s) {
    	var i = spiders.length;
    	while(i--) {
    		var curS = spiders[i];
    		if(curS == s) {
    			//удаляём поведение, т.к. мёртвые уже ничего не решают
    			delete curS.onEnterFrame;
    			spiders.splice(i, 1);
    		}
    	}
    }

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

Ctrl + Enter и смотрим результат (слайд 5). Всё должно появляться и бегать как нужно, но чего-то не хватает. Добавим хорошую фоновую графику и пытаемся почувствовать разницу (слайд 6). Имеем вполне рабочую версию игры. Пусть идея не нова, но вполне подойдёт для старта, — а там куда фантазия заведёт.

Попробую сразу же обработать возможные ошибки и вопросы:

  1. Если у вас не вылетают пули, то скорее всего вы забыли прописать во втором кадре клипа body код _parent._parent.placeBullet(dot) или в клипе body отсутствует клип dot (или просто не назван). В любом случае сверьтесь со слайдом 2.
  2. Если не появляются монстры, проверьте что есть клип ground (с нулевыми координатами в левом верхнем углу).
  3. Если не срабатывает попадание пули, проверьте что внутри клипа spider есть клип hit по которому определяется пересечение функцией hitTest().
  4. Противник умирает, но не изчезает со сцены, а анимация погибания проигрывается циклически — в последнем кадре не вызывается _parent.removeMovieClip()
  5. Если не знаете как сделать сброс игры, есть два пути: 1) удалить всех монстров, что есть на сцене (массив spiders) и вернуть главного героя в начальное положение. По хорошему нужно и пули удалить, но этим можно пренебречь, они быстро улетят за пределы экрана и удалятся сами. 2) Сделать сброс через пустой кадр как описано в этой статье (12-13 слайд).
  6. Исходник потерялся.