
Растровый рендер (анимация + движение + поворот)
Здравствуйте.
На FGB есть статьи посвященные растеризации и растровому рендеру.
1. flashgameblogs.ru/blog/actionscript/667.html
2. flashgameblogs.ru/blog/actionscript/713.html
3. flashgameblogs.ru/blog/actionscript/717.html
Спасибо авторам и надеюсь, они не против использования их статей и исходников в разработке. Копирайты из исходников сохранены. Эти 3 статьи легли в основу движка о котором эта статья.
Задача:
Двигать по полю размером 1280х1200 (видимая область флешки 640х600) юнитов с анимацией в разных направлениях с максимальной производительностью. Изначально юниты в векторе.
Решение:
Нужно объединить растеризацию MovieClip (1 статья) и растровый рендер (2 и 3).
PageUP – добавить 500 юнитов, PageDown – убрать 500 юнитов, Стрелки – двигать камеру. Правый клик – профайлер.
megaswf.com/serve/1179048
Тоже, но с фоном.
megaswf.com/serve/1179177
Весь движок состоит из 4 основных классов и 1 вспомогательного.
Что происходит в оригинальных классах более подробно можно прочитать в статьях.
Описание дано только для внесенных изменений.
1. Main.as
Код:
Можно заметить, что используется bitmap'а размеров в видимую область которая просто сдвигается и перерисовывается.
В данном классе есть возможность реализовать трансформации (в нашем случае повороты) с помощью метода draw, но этот метод слишком медленный по сравнению с copyPixels, поэтому будет применяться другой способ.
Этот способ даст нам большой выигрыш по производительности, с вполне допустимым увеличением потребляемой памяти. (В нашем случае незначительным).
2. Unit.as
Код:
Класс каждый кадр присваивает новое значение bitmapdata в зависимости от поворота, типа и состояния юнита.
Сменить состояние можно вызвав метод changeUnit(state:int);
Можно заметить, что вместо трансформации при изменении угла поворота юнита, просто изменяется bitmapdata на заранее созданную.
Всего создано 8 направлений (шаг 45 градусов), т.е. юнит ходит не точно с заданным углом, а с округленным до ближайшего кратного 45.
Все эти bitmapdata с поворотами были заранее созданы при старте в классе Rastr, что дало отличную производительность при, как писалось выше, увеличении потребляемой памяти и не точности поворота юнита. Для большей точности можно создать больше положений.
Без создания поворотов (т.е. только 1 положение) потребляемая память была 8-9 мб, при 8 положениях стала 23 мб.
Все юниты используют 1 общий набор из Rastr, поэтому растеризация проходит 1 раз при запуске и заново не создается, поэтому добавление новых юнитов происходит очень быстро и занимает мало памяти. (при 10 000 занимает +10мб)
3. Rastr.as
Код:
Перебирает Enemy_mc и отдает на растеризацию каждое состояние у каждого типа юнитов.
Специфический класс, его необходимо изменить при другой структуре передаваемого MC (в данном случае Enemy_mc).
Здесь в Enemy_mc в каждом кадре клип отражающий тип юнита с instance name = type, в каждом типе находятся кадры с клипом отображающим состояние юнита (стоит, идет, бежит, умирает и т.д.) с instance name = state, в каждом клипе состояния идет покадравая анимация которая собственно и передается на растеризацию.
4. BmpFrames.as
Код:
Можно заметить что везде используются массивы вместо вектора, на практике это оказалось быстрее на пару fps по сравнению с вектором.
5. Amath.as
Содержит в себе помимо методов из статей, дополнительно
Код:
Результат работы был показан в самом начале.
UPD1 До кучи добавил задний фон, принцип отрисовки тот же что и у юнитов. В коде подписано UPD1 возле строчек относящихся к фону (класс Main).
На FGB есть статьи посвященные растеризации и растровому рендеру.
1. flashgameblogs.ru/blog/actionscript/667.html
2. flashgameblogs.ru/blog/actionscript/713.html
3. flashgameblogs.ru/blog/actionscript/717.html
Спасибо авторам и надеюсь, они не против использования их статей и исходников в разработке. Копирайты из исходников сохранены. Эти 3 статьи легли в основу движка о котором эта статья.
Задача:
Двигать по полю размером 1280х1200 (видимая область флешки 640х600) юнитов с анимацией в разных направлениях с максимальной производительностью. Изначально юниты в векторе.
Решение:
Нужно объединить растеризацию MovieClip (1 статья) и растровый рендер (2 и 3).
PageUP – добавить 500 юнитов, PageDown – убрать 500 юнитов, Стрелки – двигать камеру. Правый клик – профайлер.
megaswf.com/serve/1179048
Тоже, но с фоном.
megaswf.com/serve/1179177
Весь движок состоит из 4 основных классов и 1 вспомогательного.
Что происходит в оригинальных классах более подробно можно прочитать в статьях.
Описание дано только для внесенных изменений.
1. Main.as
Код:
package
{
import flash.display.Bitmap;
import flash.display.BitmapData;
import flash.display.Sprite;
import flash.display.StageAlign;
import flash.display.StageScaleMode;
import flash.events.Event;
import flash.events.KeyboardEvent;
import flash.geom.Point;
import flash.geom.Rectangle;
import flash.text.TextField;
import flash.text.TextFieldAutoSize;
import flash.ui.Keyboard;
import utils.SWFProfiler;
/**
* @author Platon Skedow
* refactoring & optimization noopic
* update Marcus
*/
public class Main extends Sprite
{
public static var bitmapX:Number;
public static var bitmapY:Number;
private static const STEP:int = 500;
private var n:int;
private var textField:TextField = new TextField();
private var _stageWidth:Number;
private var _stageHeight:Number;
private var _bgWidth:Number;
private var _bgHeight:Number;
private var bitmapData:BitmapData;
private var bitmapDataBG:BitmapData;
private var bitmap:Bitmap;
private var bgRect:Rectangle;
private var rectangle:Rectangle = new Rectangle();
private var units:Array = [];
private var _rastrender:Rastr;
private var _ram:ramka_mc;
private var _ramB:ramkaB_mc;
private var _background:background_mc;
public function Main():void
{
if (stage) init();
else addEventListener(Event.ADDED_TO_STAGE, addedToStageListener);
}
private function addedToStageListener(e:Event):void
{
removeEventListener(Event.ADDED_TO_STAGE, addedToStageListener);
init();
}
private function init():void
{
SWFProfiler.init(stage, this);
_rastrender = new Rastr();
//Cоздаем экземпляр класса Rastr, в котором происходит растеризация всех нужных нам MC.
setStageParameters();
_background = new background_mc();
//UPD1 Добавляем задний фон//
createBitmap()
createTextField();
_ram= new ramka_mc();
_ramB= new ramkaB_mc();
addChild(_ram);
addChild(_ramB);
//Добавляется рамка _ramB показывающая поле 1280х1200 и рамка _ram показывающая видимую область 640х600, чтобы визуально видеть края карты.
addUnits();
addEventListener(Event.ENTER_FRAME, enterFrameListener);
stage.addEventListener(KeyboardEvent.KEY_DOWN, keyDownListener);
}
private function createTextField():void
{
textField.x = _stageWidth / 2 - 50;
textField.background = true;
textField.height = 30;
textField.selectable = false;
textField.backgroundColor = 0x000000;
textField.textColor = 0xFFFFFF;
textField.autoSize = TextFieldAutoSize.CENTER;
textField.text = n.toString();
addChild(textField);
}
private function keyDownListener(e:KeyboardEvent):void
{
if (e.keyCode == Keyboard.PAGE_UP)
addUnits();
if (e.keyCode == Keyboard.PAGE_DOWN)
removeUnits();
switch (e.keyCode)
{
case 39 :
this.x -= 20;
bitmap.x += 20;
_ram.x += 20;
textField.x += 20;
changeBitmapCord();
break; // Влево
case 37 :
this.x += 20;
bitmap.x -= 20;
_ram.x -= 20;
textField.x -= 20;
changeBitmapCord();
break; // Вправо
case 40 :
this.y -= 20;
bitmap.y += 20;
_ram.y += 20;
textField.y += 20;
changeBitmapCord();
break; // Вверх
case 38 :
this.y += 20;
bitmap.y -= 20;
_ram.y -= 20;
textField.y -= 20;
changeBitmapCord();
break; // Вниз
}
// Добавлена прокрутка карты
}
private function changeBitmapCord():void
{
bgRect.x = bitmapX = bitmap.x;
bgRect.y = bitmapY = bitmap.y;
}
// UPD1 Сохранение координат bitmap в публичные переменные вынесено отдельно
private function addUnits():void
{
n += STEP;
for (var i:int = 0; i < STEP; i++)
{
units.push(new Unit(Amath.randomRangeInt(1,5)));
}
textField.text = n.toString();
}
private function removeUnits():void
{
if (n > STEP)
{
n -= STEP;
units.splice(n, STEP);
textField.text = n.toString();
}
}
private function setStageParameters():void
{
stage.align = StageAlign.TOP_LEFT;
stage.scaleMode = StageScaleMode.NO_SCALE;
private function createBitmap():void
{
_stageWidth = 640;//App.stageWidth;
_stageHeight = 600;//App.stageHeight;
_bgWidth = 1280;//App.gameWidth;
_bgHeight = 1200;//App.gameHeight;
bitmapData = new BitmapData(_stageWidth, _stageHeight, true , 0x00000000);
bitmapDataBG = new BitmapData(_bgWidth, _bgHeight, true , 0x00000000);
bitmapDataBG.draw(_background);
bgRect = new Rectangle(0, 0, _bgWidth, _bgHeight);
// UPD1 Создаем задний фон в виде Bitmapdata, отрисовываем только видимую часть
bitmap = new Bitmap(bitmapData,"never",true );
rectangle.width = _stageWidth;
rectangle.height = _stageHeight;
addChildAt(bitmap, 0);
bitmapX = bitmap.x;
bitmapY = bitmap.y;
}
private var i:int = 0;
private var unit:Unit;
private function enterFrameListener(e:Event):void
{
bitmapData.lock();
bitmapData.fillRect(rectangle, 0x00000000);
//Можно убрать если есть фон, добавляющийся ниже и камера не выходит за границы игровой карты.
bitmapData.copyPixels(bitmapDataBG, bgRect, bgPoint, null, null, false);
// UPD1 Добавляем задний фон. Цена - потеря ~3 FPS и пары МБ памяти (тест при 10 000 юнитов). При отсутствии фона закомментировать.
while ( i < n)
{
unit = units[i];
unit.move();
if (unit._draw)
{
bitmapData.copyPixels(unit.bitmapData, unit.rectangle, unit.point, null, null, true);
}
//var matrix:Matrix = new Matrix(1, 0, 0, 1, unit.point.x, unit.point.y);
//bitmapData.draw(bullet.bitmapData, matrix, null, null, null, true);
i += 1;
}
i = 0;
bitmapData.unlock();
}
}
}
Можно заметить, что используется bitmap'а размеров в видимую область которая просто сдвигается и перерисовывается.
В данном классе есть возможность реализовать трансформации (в нашем случае повороты) с помощью метода draw, но этот метод слишком медленный по сравнению с copyPixels, поэтому будет применяться другой способ.
Этот способ даст нам большой выигрыш по производительности, с вполне допустимым увеличением потребляемой памяти. (В нашем случае незначительным).
2. Unit.as
Код:
package
{
import flash.display.BitmapData;
import flash.geom.Point;
import flash.geom.Rectangle;
/**
* @author Platon Skedow
* refactoring & optimization noopic
* update Marcus
*/
public class Unit
{
private static const MIN_SPEED:int = 1;
private static const MAX_SPEED:int = 5;
private static const RIGHT:int = 0;
private static const LEFT:int = 1;
private static const UP:int = 2;
private static const DOWN:int = 3;
private static const PADDING:Number = -20;
public static const ANGLE_STEP:Number = 45;
public var shoot:BmpFrames;
public var _allmc:Array;
private var i:int;
private var n:int=0; //Кадр клипа из массива
private var nStep:int=360/ANGLE_STEP;
// Данные для отрисовки в BitmapData
public var bitmapData:BitmapData;
public var rectangle:Rectangle;
public var point:Point = new Point();
public var _draw:Boolean = true; // флаг приносящий нам пару fps, подробнее ниже
private var destination:Point = new Point();
private var stepX:Number;
private var stepY:Number;
private var tempX:Number=0;
private var tempY:Number=0;
private var type:int; // Тип юнита (в нашем случае есть 5 разных юнитов)
private var state:int=2; // состояния юнита (идет, стоит , умирает и т.д.) у нас 2=идет
private var _rotPos:int = 0; // в какую позициу смотрит юнит
private var animationCount:Number=1;
public var animationDelay:Number=1; //замедление движения и анимации
private var gW:Number=1280;//App.gameWidth;
private var gH:Number = 1200;//App.gameHeight;
private var sW:Number=640;//App.stageWidth;
private var sH:Number=600;//App.stageHeight;
public function Unit(tp:int)
{
type = tp;
init();
}
private function init():void
{
_allmc=Rastr._enemyR[type-1];
// Присваиваем нашему локальному массиву, массив растеризованных кадров определенного типа
changeUnit(state);
//Выбираем состояние юнита, в нашем случае 2=ходьба
setPosition(point);
setNewDestination();
tempX=point.x;
tempY=point.y;
//зачем дублируем будет объесненно ниже
}
private function changeUnit(st:int):void
{
n = 0;
shoot = _allmc[st-1];
bitmapData = shoot.frames[n][_rotPos];
rectangle = shoot.frameRs[n][_rotPos];
_rtX = rectangle.x;
_rtY = rectangle.y;
i = shoot.totalFrames;
}
private function setNewDestination():void
{
setPosition(destination);
calculateStep();
changeAngle();
}
private function setPosition(point:Point):void
{
var direction:int = Amath.randomRangeInt(0, 3);
if (direction == LEFT)
{
point.x = -PADDING;
point.y = Amath.randomAdv * gH;
return;
}
if (direction == RIGHT)
{
point.x = gW + PADDING;
point.y = Amath.randomAdv * gH;
return;
}
if (direction == UP)
{
point.x = Amath.randomAdv * gW;
point.y = -PADDING;
return;
}
if (direction == DOWN)
{
point.x = Amath.randomAdv * gW;
point.y = gH + PADDING;
return;
}
}
private var distance:Number;
private var speed:Number;
private var _rtX:int;
private var _rtY:int;
private var numSteps:int;
private var distanceX:Number;
private var distanceY:Number;
private function calculateStep():void
{
speed = Amath.randomRangeNumber(MIN_SPEED, MAX_SPEED);
distance = Amath.distance(point.x,point.y,destination.x,destination.y);
numSteps = int(distance / speed);
distanceX = destination.x - point.x;
distanceY = destination.y - point.y;
if (numSteps == 0)
{
stepX = 0;
stepY = 0;
}
else
{
stepX = (distanceX / numSteps)/animationDelay;
stepY = (distanceY / numSteps)/animationDelay;
}
}
private var _angle:Number;
private function changeAngle():void
{
// находим угол поворота юнита и задаем соответствующее значение переменной _rotPos;
_angle = Amath.getAngleDeg(point.x,point.y, destination.x,destination.y);
_rotPos = Math.round(_angle / ANGLE_STEP);
if (_rotPos == nStep)
{
_rotPos=0;
}
}
public function move():void
{
point.x = tempX += stepX; //320
point.y = tempY += stepY; //300
//Центр отсчета в левом верхнем углу BitmapData, поэтому если мы захотим отобразить юнита например по центру (320,300), то он будет правее и ниже центра
// поэтому юнит смещается левее и выше (его bitmapdata) и его центр получается в нужных координатах
// оригинальные координаты сохраняются в tempX и tempY, по ним идет расчет движения юнита
// в point находятся координаты со всеми смещениями и служат для правильной визуализации (показывают точку куда копируется bitmapdata)
if ( (Amath.distance(tempX,tempY,destination.x,destination.y) )<= MAX_SPEED)
//Измеряется растояние м/у точками
{
setNewDestination();
}
if (animationCount >= animationDelay)
{
animationCount = 1;
n++;
if (n>=i)
{
n=0;
}
//Зацикливаем анимацию
point.x -= Main.bitmapX;
point.y -= Main.bitmapY;
if (point.x<=sW && point.x>=0)
{
if (point.y<=sH && point.y>=0)
{
//Основной трюк повышающий производительность, если юнит не находится в видимой области, то он и не передается в битмапу и для него не находятся //поправки и прочее что нужно для визуализации. Позволяет в разы (у меня почти в 3) поднять производительность. Минусом подхода является, то что если
//эти юниты будут находятся все разом в видимой области это вызовет падение fps (что в реальной игре врят ли случиться).
//Но зато позволяет создать карту большого размера с юнитами действующими в реальном времени.
bitmapData = shoot.frames[n][_rotPos];
point.x += Number(shoot.frameXs[n][_rotPos] + _rtX);
point.y += Number(shoot.frameYs[n][_rotPos] + _rtY);
_draw = true;
//Этот флаг нужен чтобы перерисовывать юнита только если это необходимо, а не постоянно, даже если его не видно
}
else
{
_draw = false;
}
}
else
{
_draw = false;
}
}
else
{
animationCount++;
}
}
}
}
Класс каждый кадр присваивает новое значение bitmapdata в зависимости от поворота, типа и состояния юнита.
Сменить состояние можно вызвав метод changeUnit(state:int);
Можно заметить, что вместо трансформации при изменении угла поворота юнита, просто изменяется bitmapdata на заранее созданную.
Всего создано 8 направлений (шаг 45 градусов), т.е. юнит ходит не точно с заданным углом, а с округленным до ближайшего кратного 45.
Все эти bitmapdata с поворотами были заранее созданы при старте в классе Rastr, что дало отличную производительность при, как писалось выше, увеличении потребляемой памяти и не точности поворота юнита. Для большей точности можно создать больше положений.
Без создания поворотов (т.е. только 1 положение) потребляемая память была 8-9 мб, при 8 положениях стала 23 мб.
Все юниты используют 1 общий набор из Rastr, поэтому растеризация проходит 1 раз при запуске и заново не создается, поэтому добавление новых юнитов происходит очень быстро и занимает мало памяти. (при 10 000 занимает +10мб)
3. Rastr.as
Код:
package
{
import flash.display.MovieClip;
/**
* @author Marcus
*/
public class Rastr
{
public static var _enemyR:Array;
//Массив с растеризованным MC
private var _mc:MovieClip;
public function Rastr()
{
init();
}
private function init():void
{
Enemy();
}
private function Enemy():void
{
_enemyR = [];
_mc = new Enemy_mc();
_mc.stop();
var k:int = _mc.totalFrames;
for (var j:int = 0; j < k; j++)
{
_mc.gotoAndStop(j+1);
_enemyR[j] = [];
var k1:int = _mc.type.totalFrames;
for (var g:int = 0; g <k1 ; g++)
{
_mc.type.gotoAndStop(g + 1);
_enemyR[j][g] = BmpFrames.createBmpFramesFromMC(_mc.type.state);
}
}
_mc = null;
BmpFrames.disposeScratch();
}
}
}
Перебирает Enemy_mc и отдает на растеризацию каждое состояние у каждого типа юнитов.
Специфический класс, его необходимо изменить при другой структуре передаваемого MC (в данном случае Enemy_mc).
Здесь в Enemy_mc в каждом кадре клип отражающий тип юнита с instance name = type, в каждом типе находятся кадры с клипом отображающим состояние юнита (стоит, идет, бежит, умирает и т.д.) с instance name = state, в каждом клипе состояния идет покадравая анимация которая собственно и передается на растеризацию.
4. BmpFrames.as
Код:
package
{
import flash.display.BitmapData;
import flash.display.MovieClip;
import flash.geom.Matrix;
import flash.geom.Point;
import flash.geom.Rectangle;
/**
* @author Alexander Porechnov
* update Marcus
*/
public class BmpFrames
{
public var frames :Array;
public var frameXs :Array;
public var frameYs :Array;
public var frameRs :Array;
public var totalFrames : int;
protected static var scratchBitmapData : BitmapData = null;
protected static const INDENT_FOR_FILTER : int = 64;
protected static const INDENT_FOR_FILTER_DOUBLED : int = INDENT_FOR_FILTER * 2;
protected static var scratchSize : int = 128 + INDENT_FOR_FILTER_DOUBLED;
protected static const DEST_POINT : Point = new Point(0, 0);
public function BmpFrames() {
frames = [];
frameXs = [];
frameYs = [];
frameRs = [];
totalFrames = 0;
}
private var rotationMatrix:Matrix = new Matrix();
public static function createBmpFramesFromMC(clipClass : MovieClip) : BmpFrames
{
var clip : MovieClip = clipClass;
var res : BmpFrames = new BmpFrames();
var totalFrames : int = clip.totalFrames;
var frames :Array = res.frames;
var frameXs :Array= res.frameXs;
var frameYs :Array = res.frameYs;
var frameRs :Array= res.frameRs;
var rect : Rectangle;
var flooredX : Number;
var flooredY : Number;
var mtx : Matrix = new Matrix();
var _angle:Number;
for (var i : int = 1; i <= totalFrames; i++)
{
clip.gotoAndStop(i);
var it:int = i-1;
frames[it] = [];
frameXs[it] = [];
frameYs[it] = [];
frameRs[it] = [];
for (var j:int = 0; j < 8; j++)
{
_angle = Amath.toRadians(j * 45);
//Поворачиваем и создаем массивы с уже повернутым изображением
rect = clip.getBounds(clip);
rect.width = Math.ceil(rect.width) + INDENT_FOR_FILTER_DOUBLED;
rect.height = Math.ceil(rect.height) + INDENT_FOR_FILTER_DOUBLED;
prepareScratch(rect);
flooredX = Math.floor(rect.x) - INDENT_FOR_FILTER;
flooredY = Math.floor(rect.y) - INDENT_FOR_FILTER;
mtx.rotate(_angle);
mtx.tx = -flooredX;
mtx.ty = -flooredY;
scratchBitmapData.draw(clip, mtx, null, null, null, false);
mtx.identity();
var trimBounds : Rectangle = scratchBitmapData.getColorBoundsRect(0xFF000000, 0x00000000, false);
trimBounds.x -= 1;
trimBounds.y -= 1;
trimBounds.width += 2;
trimBounds.height += 2;
var bmpData : BitmapData = new BitmapData(trimBounds.width, trimBounds.height, true, 0);
bmpData.copyPixels(scratchBitmapData, trimBounds, DEST_POINT);
flooredX += trimBounds.x;
flooredY += trimBounds.y;
frames[it][j]=bmpData;
frameXs[it][j]=flooredX;
frameYs[it][j]=flooredY;
frameRs[it][j] = rect;
}
}
res.totalFrames = res.frames.length;
return res;
}
public static function disposeScratch() : void {
scratchBitmapData.dispose();
scratchBitmapData = null;
}
protected static function prepareScratch(rect : Rectangle) : void {
var sizeIncreased : Boolean = false;
while (rect.width >= scratchSize || rect.height >= scratchSize) {
scratchSize *= 2;
sizeIncreased = true;
}
if (scratchBitmapData != null && sizeIncreased) {
disposeScratch();
}
if (scratchBitmapData == null) {
scratchBitmapData = new BitmapData(scratchSize, scratchSize, true, 0);
} else {
scratchBitmapData.fillRect(scratchBitmapData.rect, 0);
}
}
}
}
Можно заметить что везде используются массивы вместо вектора, на практике это оказалось быстрее на пару fps по сравнению с вектором.
5. Amath.as
Содержит в себе помимо методов из статей, дополнительно
Код:
public static function distance(x1:Number, y1:Number, x2:Number, y2:Number):Number
{
var dx:Number = x2 - x1;
var dy:Number = y2 - y1;
return Math.sqrt(dx * dx + dy * dy);
}
public static function getAngleDeg(x1:Number, y1:Number, x2:Number, y2:Number, norm:Boolean = true):Number
{
var dx:Number = x2 - x1;
var dy:Number = y2 - y1;
var angle:Number = Math.atan2(dy, dx) / Math.PI * 180;
if (norm)
{
if (angle < 0)
{
angle = 360 + angle;
}
else if (angle >= 360)
{
angle = angle - 360;
}
}
return angle;
}
Результат работы был показан в самом начале.
UPD1 До кучи добавил задний фон, принцип отрисовки тот же что и у юнитов. В коде подписано UPD1 возле строчек относящихся к фону (класс Main).
- +19
- Marcus
Комментарии (11)
Так значительно лучше чем на форуме.
А по поводу бага в исходниках, думаю это не есть хорошо. ИМХО лучше убрать.
Делалось под 10+ версию плеера, профайлер стандартный. Можно stats из стать приделать.
А то я никак не успевал закончить статейку :)
Кстати, а зачем использовать самописный код для определения расстояния? Point.distance удобнее.
В проекте текущем используется, вот и сюда прилепил )) Сделано было чтоб везде расстояние определялось одинаковой строчкой, а не каждый по разному. Смысл одинаковый.