понедельник, 26 апреля 2010 г.

Определение столкновений объектов неправильной формы

Представляю вашему вниманию вольный перевод раздела «Hit Testing Irregularly Shaped Objects» из книги AdvancED ActionScript 3.0 Animation, автор Keith Peters.

Сегодня речь пойдет об определении столкновений объектов неправильной формы.

Например таких:

Звезды не пересекаются


Шаг 1. Предисловие

В книге Making Things Move (здесь и далее автор ссылается на другую свою книгу Foundation ActionScript Animation: Making Things Move! - очень интересная книга для начинающих, содержит самые основы) мы рассмотрели несколько основных методов определения столкновения, основанных на использовании методов hitTestObject () и hitTestPoint (), а также метод, основанный на определении расстояний между объектами. Использование каждого из этих методов зависит от формы объектов, столкновение которых вы проверяете. Метод hitTestObject () больше подходит для определения столкновений между двумя объектами прямоугольной формы, а при проверке объектов другой формы будет часто возвращать ошибки. Метод hitTestPoint () подходит для определения положения курсора мыши над тем или иным объектом, или пересечения объекта-точки с объектом другой формы, но мало подходит для двух больших объектов. Метод, основанный на определении расстояний, прекрасно работает с круглыми объектами, но будет часто пропускать столкновения объектов другой формы.

Святой Грааль определения столкновений в Flash – проверка любых двух объектов неправильной формы и точное знание касаются ли они друг друга или нет. Для этого используется класс BitmapData (появился в FlashPlayer 8.0) и его метод hitTest().

ActionScript содержит BitmapData класс, который содержит фактическое растровое изображение и класс Bitmap , который служит для отображения этого растрового изображения на экране.
Метод hitTest() класса BitmapData сравнивает два экземпляра класса BitmapData и говорит о том, перекрываются ли любые их пиксели.
Когда вы создаете экземпляр класса BitmapData, вы определяете в его конструкторе, поддерживает ли он прозрачность:

new BitmapData(width, height, transparent, color);



Присваивая параметру transparent булево значение (true/false) вы устанавливаете поддержку прозрачности. Если вы устанавливаете значение false, то изображение будет непрозрачным. После создания, объект будет представлять собой прямоугольник, залитый указанным цветом фона (параметр color). Вы можете использовать различные методы класса BitmapData для изменения пикселей растрового изображения, но оно будет всегда полностью непрозразным и закрывать все объекты, которые находятся позади этого изображения в списке отображения. Значение цвета для каждого пикселя (при значении transparent равном false) определяется 24-битным значением в форме 0хRRGGBB, где RR, GG, BB – двухразрядные шестнадцатеричные числа представляющие красный, зеленый и синий каналы значением от 00 (0) до FF(255). Например, 00хFFFFFF – белый цвет, 00xFF0000 - красный, 00xFF9900 – оранжевый. Для записи и извлечения значений каждого пикселя используются методы setPixel() и getPixel() с 24-битным значением цвета.

Однако если вы присваиваете параметру transparent значение true, то каждый пиксель растрового изображения поддерживает альфа-канал (прозрачность) и использует для записи цвета 32-битное значение в формате 00xAARRGGBB. Здесь первые два знака определяют значение альфа-канала (уровень прозрачности), при значении 00 пиксель полностью прозрачен, при значении FF – полностью непрозрачен. В прозрачном экземпляре класса BitmapData, для записи и чтения значений каждого пикселя необходимо использовать методы setPixel32() и getPixel32(). Наш метод определения столкновений будет использовать 32-битные значения. Заметим, что если использовать в методах setPixel32() и getPixel32() 24-битные значения цвета, то альфа-канал получает значение 0.

Для того, чтобы увидеть различие между прозрачным и непрозрачным растровыми изображениями, создадим каждый из них.
package
{
import flash.display.Bitmap;
import flash.display.BitmapData;
import flash.display.Sprite;
import flash.display.StageAlign;
import flash.display.StageScaleMode;
import flash.geom.Rectangle;

public class BitmapCompare extends Sprite
{
public function BitmapCompare()
{
stage.align = StageAlign.TOP_LEFT;
stage.scaleMode = StageScaleMode.NO_SCALE;

// отрисовываем случайные линии
graphics.lineStyle(0);
for(var i:int = 0; i < 100; i++)
{
graphics.lineTo(Math.random() * 300, Math.random() * 400);
}

// создаем непрозрачное растровое изображение
var bmpd1:BitmapData = new BitmapData(300, 200, false, 0xffffff);
bmpd1.fillRect(new Rectangle(100, 50, 100, 100), 0xff0000);
var bmp1:Bitmap = new Bitmap(bmpd1);
addChild(bmp1);

// создаем прозрачное растровое изображение
var bmpd2:BitmapData = new BitmapData(300, 200, true, 0x00ffffff);
bmpd2.fillRect(new Rectangle(100, 50, 100, 100), 0x80ff0000);
var bmp2:Bitmap = new Bitmap(bmpd2);
bmp2.y = 200;
addChild(bmp2);
}
}
}

Сначала этот код отрисовывает связку случайных линий, чтобы вы могли отличить растровое изображение от другого содержимого stage. Затем создаем два растровых изображения и отрисовываем два красных квадрата в центре каждого из них. Верхнее растровое изображение полностью непрозрачно и закрывает собой все случайные линии. Нижнее растровое изображение прозрачно, но красный квадрат, который содержится в этом изображении, закрывает линии на stage (см рисунок 1).


Рисунок 1. – Прозрачное растровое изображение снизу и непрозрачное сверху



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

bmpd2.fillRect(new Rectangle(100, 50, 100, 100), 0xffff0000);

следующим образом:

bmpd2.fillRect(new Rectangle(100, 50, 100, 100), 0x80ff0000);


Заметим, что здесь мы используем 32-битное значение AARRGGBB цвета для заливки и значение альфа-канала 0х80 дает нам полупрозрачность красного квадрата (см рисунок 2).

Полупрозрачный квадрат
Рисунок 2. – Полупрозрачный квадрат



Шаг 2. Растровые изображения для определения столкновений

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

package
{
import flash.display.Sprite;

public class Star extends Sprite
{
public function Star(radius:Number, color:uint = 0xFFFF00):void
{
graphics.lineStyle(0);
graphics.moveTo(radius, 0);
graphics.beginFill(color);
// draw 10 lines
for(var i:int = 1; i < 11; i++) { var radius2:Number = radius; if(i % 2 > 0)
{
//изменяем радиус, чтобы сделать лучи звезды
radius2 = radius / 2;
}
var angle:Number = Math.PI * 2 / 10 * i;
graphics.lineTo(Math.cos(angle) * radius2, Math.sin(angle) * radius2);
}
}
}
}


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

А теперь класс, который проверяет столкновения.

package
{
import flash.display.Bitmap;
import flash.display.BitmapData;
import flash.display.Sprite;
import flash.display.StageAlign;
import flash.display.StageScaleMode;
import flash.events.MouseEvent;
import flash.filters.GlowFilter;
import flash.geom.Matrix;
import flash.geom.Point;

public class BitmapCollision1 extends Sprite
{
private var bmpd1:BitmapData;
private var bmp1:Bitmap;
private var bmpd2:BitmapData;
private var bmp2:Bitmap;

public function BitmapCollision1()
{
stage.align = StageAlign.TOP_LEFT;
stage.scaleMode = StageScaleMode.NO_SCALE;

// создаем звезду
var star:Star = new Star(50);

// создаем неподвижный экземпляр класса Bitmap и
//отрисовываем в нем звезду
bmpd1 = new BitmapData(100, 100, true, 0);
bmpd1.draw(star, new Matrix(1, 0, 0, 1, 50, 50));
bmp1 = new Bitmap(bmpd1);
bmp1.x = 200;
bmp1.y = 200;
addChild(bmp1);

// создаем подвижный экземпляр класса Bitmap и
//отрисовываем в нем звезду
bmpd2 = new BitmapData(100, 100, true, 0);
bmpd2.draw(star, new Matrix(1, 0, 0, 1, 50, 50));
bmp2 = new Bitmap(bmpd2);
addChild(bmp2);

stage.addEventListener(MouseEvent.MOUSE_MOVE, onMouseMoving);
}

private function onMouseMoving(event:MouseEvent):void
{
// move bmp2 to the mouse position (centered).
bmp2.x = mouseX - 50;
bmp2.y = mouseY - 50;

// проверяем столкновение.
if(bmpd1.hitTest(new Point(bmp1.x, bmp1.y), 255, bmpd2, new Point(bmp2.x, bmp2.y), 255))
{
//реакция на столкновение
bmp1.filters = [new GlowFilter()];
bmp2.filters = [new GlowFilter()];
}
else
{
bmp1.filters = [];
bmp2.filters = [];
}
}
}
}


Здесь мы создаем звезду, использую наш класс Star и отрисовываем ее в двух экземплярах класса Bitmap. В методе draw() мы используем объект Matrix для перемещения нашей звезды на 50 пикселей вправо и вниз, т.к. точка регистрации нашей звезды находится в ее центре, а точка регистрации экземпляра класса Bitmap в верхнем левом углу. Благодаря этому мы можем видеть целую звезду, а не ее часть. Один из экземпляров класса Bitmap мы делаем неподвижным, а другой движущимся за курсором мыши. Ключевая строка выглядит следующим образом:

if(bmpd1.hitTest(new Point(bmp1.x, bmp1.y), 255, bmpd2, new Point(bmp2.x, bmp2.y), 255))


Это – то, что фактически определяет, столкнулись ли два объекта. Сигнатура метода hitTest() выглядит так:

public function hitTest(firstPoint:Point,
firstAlphaThreshold:uint,
secondObject:Object,
secondBitmapDataPoint:Point = null,
secondAlphaThreshold:uint = 1):Boolean


Обратите внимание, параметры этого метода разбиты на две группы: определяющие первый объект и определяющие второй объект. Вы задаете значение для каждой точки. Они определяют верхний левый угол экземпляра класса BitmapData. Точки задаются потому, что каждое растровое изображение может быть вложено в другой символ или множество символов. В этом случае они могли бы находиться в совершенно разных координатных системах. Определение точки дает вам возможность соотнести между собой эти разные координатные системы, возможно используя метод localToGlobal() класса DisplayObject. В этом примере, оба растровых изображения будут находиться прямо на stage, поэтому для задания точек мы сможем просто использовать их позиции.

Параметры firstAlphaThreshold и secondAlphaThreshold служат для определения значений альфа-канала для обоих объектов. Как вы видели ранее, каждый пиксель в прозрачном экземпляре класса BitmapData может иметь значение от 0 (полная прозрачность) до 255 (полная непрозрачность). Значения firstAlphaThreshold и secondAlphaThreshold определяют порог прозрачности пикселя, при котором должно быть зарегистрировано пересечение. В этом примере мы используем значение 255 для каждого параметра, тем самым подразумевая, что пересечение должно быть зарегистрировано для полностью непрозрачных объектов.

Наконец параметр secondObject. Заметим, что типом этого параметра является класс Object. Здесь вы можете использовать экземпляры классов Point, Rectangle или другой экземпляр класса BitmapData. Если вы используете экземпляр класса Point или Rectangle вам не нужно использовать два последних параметра secondBitmapDataPoint и secondAlphaThreshold. Использование экземпляра класса Point для определения пересечения позиции курсора мыши и произвольного экземпляра класса BitmapData представлено ниже:

if(myBitmapData.hitTest(new Point(myBitmapData.x, myBitmapData.y),
255,
new Point(mouseX, mouseY)))
{
//курсор мыши пересекается с myBitmapData
}


В нашем примере мы используем в качестве второго объекта другой экземпляр класса BitmapData, поэтому мы должны определить параметры secondBitmapDataPoint и secondAlphaThreshold.

Наконец, если произошло столкновение, мы для каждой звезды применяем фильтр GlowFilter, если столкновения нет, то мы удаляем фильтр. Результат представлен на рисунке 3 и рисунке 4.

Звезды не пересекаются
Рисунок 3. – Звезды не пересекаются



Звезды пересекаются
Рисунок 4. – Звезды пересекаются





Шаг 3. Использование метода BitmapData.hitTest() для проверки нерастровых изображений

Ранее мы использовали экземпляры класса Bitmap напрямую, как отображаемые объекты, которые мы перемещали и проверяли на столкновение друг с другом. Но чаще вы будете иметь дело с отображаемыми объектами таких типов как MovieClip, Sprite, Shape. Поскольку вы не можете использовать метод BitmapData.hitTest() для проверки объектов этих типов вам нужно немного пересмотреть сам принцип. Нам нужно всего лишь поместить наши объекты в экземпляры класса BitmapData, которые не могут быть добавлены в список отображения, и проверять на столкновения эти экземпляры-оболочки.

package
{
import flash.display.BitmapData;
import flash.display.Sprite;
import flash.display.StageAlign;
import flash.display.StageScaleMode;
import flash.events.MouseEvent;
import flash.filters.GlowFilter;
import flash.geom.Matrix;
import flash.geom.Point;

public class BitmapCollision3 extends Sprite
{
private var bmpd1:BitmapData;
private var bmpd2:BitmapData;
private var star1:Star;
private var star2:Star;

public function BitmapCollision3()
{
stage.align = StageAlign.TOP_LEFT;
stage.scaleMode = StageScaleMode.NO_SCALE;

//создаем две звезды и добавляем их в список отображения
star1 = new Star(50);
addChild(star1);

star2 = new Star(50);
star2.x = 200;
star2.y = 200;
addChild(star2);

// создаем два растровых изображения
bmpd1 = new BitmapData(stage.stageWidth, stage.stageHeight, true, 0);
bmpd2 = bmpd1.clone();

stage.addEventListener(MouseEvent.MOUSE_MOVE, onMouseMoving);
}

private function onMouseMoving(event:MouseEvent):void
{
// перемещаем star1 в позицию курсора мыши
star1.x = mouseX;
star1.y = mouseY;

// очищаем растровые изображения
bmpd1.fillRect(bmpd1.rect, 0);
bmpd2.fillRect(bmpd2.rect, 0);

// отрисовываем каждую звезду в своем растровом изображении
bmpd1.draw(star1, new Matrix(1, 0, 0, 1, star1.x, star1.y));
bmpd2.draw(star2, new Matrix(1, 0, 0, 1, star2.x, star2.y));

// проверяем столкновение
if(bmpd1.hitTest(new Point(), 255, bmpd2, new Point(), 255))
{
star1.filters = [new GlowFilter()];
star2.filters = [new GlowFilter()];
}
else
{
star1.filters = [];
star2.filters = [];
}
}
}
}


В этот раз, в конструкторе мы создаем два экземпляра класса BitmapData и две звезды. Сейчас нам не нужно помещать экземпляры класса BitmapData в экземпляры класса Bitmap, для того, чтобы они попали в список отображения. Звезды в список отображения мы добавляем. Первая звезда, star1, перемещается за курсором мыши. Каждый раз, когда курсор мыши перемещается, мы присваиваем координаты курсора мыши, координатам нашей первой звезды, затем наше растровое изображение очищаем от звезды, которая была отрисована в предыдущий момент перемещения мыши с помощью метода fillRect(), с установленным значением цвета равным нулю. Помните, что если значение альфа-канала не определено, то оно принимается равным нулю, в результате мы имеем все пиксели объекта BitmapData полностью прозрачными. Теперь мы отрисовываем каждую звезду в соответствующем растровом изображении:

bmpd1.draw(star1, new Matrix(1, 0, 0, 1, star1.x, star1.y));
bmpd2.draw(star2, new Matrix(1, 0, 0, 1, star2.x, star2.y));


Объекты Matrix мы используем для отрисовки звезд в том же самом положении, в котором они находятся на stage. Теперь мы можем проверить объекты на столкновение:

if(bmpd1.hitTest(new Point(), 255, bmpd2, new Point(), 255))


Поскольку обе звезды находятся в одном и том же координатном пространстве и отрисованы в соответствующих объектах BitmapData, координаты которых совпадают с координатами звезд, мы не должны делать какие либо корректировки координатных пространств. Мы только задаем новые объекты Point по умолчанию (с значениями х и у равными нулю). Также мы задаем значение альфа-каналу равным 255, поэтому будет проверятся столкновение только полностью непрозрачных участков наших объектов BitmapData, а это и есть как раз наши звезды.

На этом у меня все.

исходники

1 комментарий:

  1. Добрый день.А можно к оригиналу со звездами показать на примере если звезда вращается.Все перепробывал,получается полное не совпадение.Вращаю звезду и дописываю такой код: var myMatrix:Matrix = new Matrix(); myMatrix.scale(star1.scaleX, star1.scaleY); myMatrix.rotate(star1.rotation); myMatrix.translate(star1.x, star1.y); Но при этом столкновение то есть то нет,то есть там где не должно быть.Спасибо.С уважением Максим.

    ОтветитьУдалить