суббота, 31 июля 2010 г.

Создание игры “Asteroids”

Представляю вашему вниманию перевод серии туториалов Tutorial: Using Flash CS3 and Actionscript 3 to create Atari 7800 Asteroids

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

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

Вот подобие того, что должно получиться

[SWF]http://coolisee.com/wordpress/wp-content/uploads/2010/07/31/7800asteroids4.swf, 400, 400[/SWF]




Создание игры «Asteroids». Часть 1



Начнем

Я собираюсь написать весь код игры в одном классе с названием Main. Я не планирую использовать любые другие классы для игровых объектов.
Main будет классом документа моего fla – файла. В библиотеке этого файла есть четыре элемента (см рисунок 1).

Символы в библиотеке
Рисунок 1. – Символы в библиотеке

background это векторное изображение звездного неба размером 400х400 пикселей. В поле «Instance name» присвоим ему имя Background. Я хочу на примере составного фона показать как блитировать игровые объекты с прозрачными пикселями на передний план. Это не очень сложное изображение, но его хватит для простейшей демонстрации. Звезды здесь сделаны с помощью клипа star.

Изображение фона
Рисунок 2. – Изображение фона

Клип ship содержит изображение flash0.png в формате png размером 20x20 пикселей. Координаты этого изображения внутри клипа составляют (-10, -10). В поле «Instance name» присвоим ему имя Ship. Центр этого клипа должен находиться посередине. Это будет точка, вокруг которой наш клип будет вращаться.

Клип корабля
Рисунок 3. - Клип корабля

Stage нашего документа белого цвета и размером 400х400 пикселей, частота кадров 120. Это значение выбрано для того, чтобы показать преимущество «кэшированной» анимации перед обычной. Классом документа будет наш класс Main.


Рисунок 4

Класс Main

Класс Main будет храниться в файле Main.as. Мы подробно разберем все ключевые части этого класса ниже.

package {

import flash.display.Bitmap;
import flash.display.MovieClip;
import flash.events.TimerEvent;
import flash.utils.Timer;
import flash.display.BitmapData;
import flash.geom.*;
import flash.events.KeyboardEvent;

public class Main extends MovieClip {

var aRotation:Array=[];
var aShipAnimation:Array=[];
var shipAnimationArrayLength:int;
var ship:Ship;
var shipHolder:MovieClip;
var animationTimer:Timer;
var animationCounter:int;
var playerObject:Object;
var canvasBD:BitmapData;
var canvasBitmap:Bitmap;
var backgroundBD:BitmapData;
var backgroundSource:Background;
var gameTimer:Timer;
var backgroundRect:Rectangle;
var backgroundPoint:Point;
var playerRect:Rectangle;
var playerPoint:Point;
var aKeyPress:Array=[];
var spriteHeight:int=20;
var spriteWidth:int=20;


public function Main() {
trace("main");
createObjects();
createRotationArray();
createShipAnimation();

}


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

		private function createObjects():void {
//create generic object for the player
playerObject=new Object();
playerObject.arrayIndex=0;
playerObject.x=200;
playerObject.y=200;
playerObject.dx=0;
playerObject.dy=0;
playerObject.movex=0;
playerObject.movey=0;
playerObject.acceleration=.3;
playerObject.maxVelocity=8;
playerObject.friction=.01;
playerObject.centerx=playerObject.x+spriteWidth;
playerObject.centery=playerObject.y+spriteHeight;
playerRect=new Rectangle(0,0,spriteWidth*2,spriteHeight*2);
playerPoint=new Point(playerObject.x,playerObject.y);

//init canvas and display bitmap for canvas
canvasBD=new BitmapData(400,400,false,0x000000);
canvasBitmap=new Bitmap(canvasBD);

//init background
backgroundSource=new Background();
backgroundBD=new BitmapData(400,400,false,0x000000);
backgroundBD.draw(backgroundSource,new Matrix());
backgroundRect=new Rectangle(0,0,400,400);
backgroundPoint=new Point(0,0);

}


Метод createObjects() создает объект playerObject для хранения свойств нашего корабля.
playerObject.arrayIndex=0; - это текущий индекс анимации для корабля. Мы будем хранить в массиве 36 изображений в BitmapData. Это свойство будет индексом изображения в этом массиве. Мы будем также хранить предрасчетный массив значений смещений по х и у для использования при отображении. Этот индекс будет использоваться и при работе с этим массивом.
playerObject.x=200;
playerObject.y=200; - здесь мы помещаем наш корабль в точку с координатами (200,200).
playerObject.dx=0;
playerObject.dy=0; - здесь храним текущие смещения по осям х и у, которые будем ассоциировать с направлением движения корабля на экране. Корабль неподвижен в начале игры, поэтому эти значения равны нулю.
playerObject.movex=0;
playerObject.movey=0; - здесь храним текущую скорость и направление корабля.
playerObject.acceleration=.3; - это число пикселей, добавляемое к значениям х и у корабля, если нажата кнопка UP.
playerObject.maxVelocity=8; - это максимально возможная скорость по направлениям х и у.
playerObject.friction=.01; - сопротивление воздуха, которое действует на корабль и препятствует бесконечному движению по инерции.
playerObject.centerx=playerObject.x+spriteWidth;
playerObject.centery=playerObject.y+spriteHeight; - мы будем хранить значения координат центра нашего корабля, чтобы увеличить скорость вычислений
playerRect=new Rectangle(0,0,spriteWidth*2,spriteHeight*2);
playerPoint=new Point(playerObject.x,playerObject.y); - эти переменные будут использоваться для блитирования с помощью метода copyPixels() (см сигнатуру метода). Мы создаем их здесь, чтобы потом не создавать в каждом кадре.

Теперь мы создадим холст для блитирования в него всех объектов.

canvasBD=new BitmapData(400,400,false,0x000000);
canvasBitmap=new Bitmap(canvasBD);


canvasBD это полностью непрозрачный экземпляр BitmapData, в котором будет храниться изображение всего экрана. canvasBitmap – единственный отображаемый объект в игре. Он хранит в себе холст canvasBD, в который блитированы пиксели изображений фона и корабля.

Наконец, мы создаем наш фон

backgroundSource=new Background();
backgroundBD=new BitmapData(400,400,false,0x000000);
backgroundBD.draw(backgroundSource,new Matrix());
backgroundRect=new Rectangle(0,0,400,400);
backgroundPoint=new Point(0,0);


Фон создается путем создания экземпляра объекта Background из нашей библиотеки. Это непрозрачный мувиклип размером 400х400 пикселей. Т.к. фон в нашей библиотеке это векторный рисунок, нам нужно отрисовать его в BitmapData с помощью метода draw(). Переменные backgroundRect и backgroundPoint создаем и вычисляем их значения заранее опять же для того, чтобы не вычислять их в каждом кадре.

		private function createRotationArray():void {
shipAnimationArrayLength=36;
var rotation:int=0;
for (var ctr:int=0;ctr var tempObject:Object={};
tempObject.dx=Math.cos(2.0*Math.PI*(rotation-90)/360.0);
tempObject.dy=Math.sin(2.0*Math.PI*(rotation-90)/360.0);
trace(ctr+":dx=" + Math.cos(2.0*Math.PI*(rotation-90)/360.0));
trace(ctr+":dy=" + Math.sin(2.0*Math.PI*(rotation-90)/360.0));
aRotation.push(tempObject);
rotation+=10;
}

trace("aRotation[5].dx=" + aRotation[5].dx);
}


Метод createRotationArray() используется для предрасчета. 36 углов (по 10 градусов) соответствуют полной окружности (360 градусов). Если вы хотите создать более точную анимацию, вам нужно изменить значение shipAnimationArrayLength например на 180 и выражение rotation +=10 на rotation +=2. Цикл вычисляет значения dx и dy в радианах и сохраняет их в свойствах объекта, который в свою очередь помещается в массив aRotation.

		private function createShipAnimation():void {
shipHolder=new MovieClip();
shipHolder.x=50;
shipHolder.y=50;
ship=new Ship();
ship.x=spriteWidth;
ship.y=spriteHeight;
shipHolder.addChild(ship);
addChild(shipHolder);
animationCounter=0;
animationTimer=new Timer(1,36);
animationTimer.addEventListener(TimerEvent.TIMER, cacheShip);
animationTimer.addEventListener(TimerEvent.TIMER_COMPLETE, animationTimerCompleteHandler);
animationTimer.start();
}


Метод createShipAnimation() создает анимацию нашего корабля. Судно появляется на экране в самом начале с rotation = 0, и будет поворачиваться на 10 градусов в каждой итерации, пока все 36 изображений не будут сохранены в массиве экземпляров BitmapData. Сначала мы создаем отображаемый объект shipHolder (в данном случае это MovieClip) для хранения в нем экземпляра Ship из библиотеки. Зачем это делать? Дело в том, что BitmapData для отрисовки использует только положительную часть системы координат. Вращая объект вокруг его центра, половина этого объекта будет находиться в отрицательной части. Так мы поместили изображение корабля в png в мувиклип с координатами -10,-10 и при вращении ship без контейнера shipHolder мы не получили бы пикселей изображения, которое находиться в отрицательной части системы координат. Итак, мы помещаем ship в контейнер shipHilder и вращаем ship внутри контейнера.

Мы создаем экземпляр Ship называем его ship и присваиваем координатам х и у значения spriteWidth и spriteHeight соответственно. В этом случае это 20,20. Это позволит нам создать объект BitmapData размером 40х40, в котором запишутся полные изображения целиком без срезов и скосов.

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

		private function cacheShip(e:TimerEvent):void {
var spriteMapBitmap:BitmapData=new BitmapData(spriteWidth*2,spriteHeight*2,true,0x00000000);
spriteMapBitmap.draw(shipHolder,new Matrix());
aShipAnimation.push(spriteMapBitmap);
trace("caching " + animationCounter);
animationCounter++;
ship.rotation+=10;
}

private function animationTimerCompleteHandler(e:TimerEvent):void {
startGame();
}


После всех установок метод cacheShip() относительно прост. В каждой из 36 итераций таймера он создает новый, поддерживающий прозрачность, объект BitmapData, названный spriteMapBitmap, размер которого составляет 40х40 пикселей (spriteWidth*2,spriteHeight*2). Затем мы перерисовываем пиксели из shipHolder в эту битмапдату с помощью метода draw(). Затем эту битмапдату сохраняем в массив aShipAnimation и изменяем значение угла поворота ship на 10 градусов.

Метод animationTimerCompleteHandler будет вызван, когда все 36 итераций будут выполнены. На этом мы закончим с кэшированием анимации в этой части. Далее мы добавим анимацию астероидов, ракет и взрывов.

		private function startGame():void {
removeChild(shipHolder);
addChild(canvasBitmap);

stage.addEventListener(KeyboardEvent.KEY_DOWN,keyDownListener);
stage.addEventListener(KeyboardEvent.KEY_UP,keyUpListener);

gameTimer=new Timer(50);
gameTimer.addEventListener(TimerEvent.TIMER, runGame);
gameTimer.start();

}

private function runGame(e:TimerEvent) {
checkKeys();
updatePlayer();
drawBackground();
drawPlayer();
}


Метод startGame() начинает игровой цикл. Цикл – метод, названный runGame(), который вызывается каждые 50 миллисекунд. Это значение можно увеличить или уменьшить по необходимости. Мы делаем это с помощью класса Timer, хотя можно было использовать и событие Event.ENTER_FRAME. Мы также добавляем слушателей к событиям нажатий кнопок KEY_UP и KEY_DOWN и удаляем из списка отображения shipHolder и добавляем в этот список холст canvasBitmap.

Этот холст содержит BitmapData размером 400х400, в которую отрисованы все битмапдаты наших изображений. Этот холст ЕДИНСТВЕННЫЙ отображаемый объект в нашей игре.
Метод runGame() предельно прост. Он проверяет изменения во вводе с клавиатуры, обновляя позицию судна игрока, основанную на значениях нажатых клавиш и инерции, и отрисовывая объекты на экране в требуемой позиции. Сначала мы отрисовываем фон и затем сам корабль.

		private function keyDownListener(e:KeyboardEvent) {
//trace("down e.keyCode=" + e.keyCode);
aKeyPress[e.keyCode]=true;

}

private function keyUpListener(e:KeyboardEvent) {
//trace("up e.keyCode=" + e.keyCode);
aKeyPress[e.keyCode]=false;
}

private function checkKeys():void {
if (aKeyPress[38]){
trace("up pressed");
playerObject.dx=aRotation[playerObject.arrayIndex].dx;
playerObject.dy=aRotation[playerObject.arrayIndex].dy;

var mxn:Number=playerObject.movex+playerObject.acceleration*(playerObject.dx);
var myn:Number=playerObject.movey+playerObject.acceleration*(playerObject.dy);

var currentSpeed:Number = Math.sqrt ((mxn*mxn) + (myn*myn));
if (currentSpeed < playerObject.maxVelocity) {
playerObject.movex=mxn;
playerObject.movey=myn;
} // end speed check


}
if (aKeyPress[37]){
playerObject.arrayIndex--;
if (playerObject.arrayIndex <0) playerObject.arrayIndex=shipAnimationArrayLength-1;

}
if (aKeyPress[39]){
playerObject.arrayIndex++;
if (playerObject.arrayIndex ==shipAnimationArrayLength) playerObject.arrayIndex=0;

}
}


Чтобы избежать паузы, которая создается, если какая-либо кнопка удерживается, мы создадим массив aKeyPress, который будет действовать как старые методы AS2 key.up и key.down. Когда клавиша нажата (СТРЕЛКА ВВЕРХ keyCode = 38, СТРЕЛКА ВЛЕВО keyCode = 38, СТРЕЛКА ВПРАВО keyCode = 38) мы сохраняем булево значение true в массиве с индексом равным значению keyCode нажатой клавиши. Когда клавиша отпущена, то этому значению присваиваем false.

Метод checkKeys() относительно сложен, поскольку содержит большую часть логики для движения и вращения корабля. Во-первых, если удерживается клавиша СТРЕЛКА ВВЕРХ (if (aKeyPress[38])) нам нужно определить значения dx и dy объекта playerObject соответствующие данному направлению движения. У нас уже есть массив этих предрасчитанных значений. Текущий индекс playerObject.arrayIndex может равняться числу от 0 до 35 и зависит от текущего значения поворота объекта. Этот же индекс используется в массиве aRotation, для нахождения соответствующих dx и dy, которые прибавляются к переменным playerObject.movex и playerObject.movey. Когда игра только начинается, значения этих переменных равны нулю, но как только нажимается клавиша СТРЕЛКА ВВЕРХ эти значения вычисляются в следующих строчках:

var mxn:Number=playerObject.movex+playerObject.acceleration*(playerObject.dx);
var myn:Number=playerObject.movey+playerObject.acceleration*(playerObject.dy);


Мы создаем две временных переменных для хранения новых значений movex и movey, которые названы mxn и myn соответственно. Для создания новых значений мы добавляем значение ускорения, умноженное на dx или dy к текущим значениям movex и movey. Теперь нам нужно проверить значение текущей скорости currentSpeed с значением максимальной скорости объекта playerObject.maxVelocity. Если текущая скорость меньше максимальной, то мы присваиваем playerObject.movex и playerObject.movey новые значения, если нет, то ничего не делаем.

Последние две проверки нажатия делаются для клавиш СТРЕЛКА ВПРАВО и СТРЕЛКА ВЛЕВО. Здесь мы изменяем значения playerObject.arrayIndex тем самым изменяя значения dx и dy. И проверяем это значение playerObject.arrayIndex, чтобы они не вышло за допустимые пределы.

		private function updatePlayer():void {

//add friction

if (playerObject.movex > 0) {
playerObject.movex-=playerObject.friction;
}else if (playerObject.movex < 0) {
playerObject.movex+=playerObject.friction;
}

if (playerObject.movey > 0) {
playerObject.movey-=playerObject.friction;
}else if (playerObject.movey < 0) {
playerObject.movey+=playerObject.friction;
}

playerObject.x+=(playerObject.movex);
playerObject.y+=(playerObject.movey);

playerObject.centerx=playerObject.x+spriteWidth;
playerObject.centery=playerObject.y+spriteHeight;

if (playerObject.centerx > stage.width) {
playerObject.x=-spriteWidth;
playerObject.centerx=playerObject.x+spriteWidth;
}else if (playerObject.centerx < 0) {
playerObject.x=stage.width - spriteWidth;
playerObject.centerx=playerObject.x+spriteWidth;
}

if (playerObject.centery > stage.height) {
playerObject.y=-spriteHeight;
playerObject.centery=playerObject.y+spriteHeight;
}else if (playerObject.centery < 0) {
playerObject.y=stage.height-spriteHeight;
playerObject.centery=playerObject.y+spriteHeight;
}

trace("centerx=" + playerObject.centerx);
}


Метод updatePlayer() вызывается при каждом вызове runGame(), чтобы удостовериться, что корабль помещен на экран в правильной позиции. Эта позиция изменяется в большинстве интервалов, т.к. у нас есть реакция трения и инерция. Первое что мы делаем, это добавление или вычитание к скорости (в зависимости от его значения) значения этого самого трения. Мы делаем это в каждом интервале и таким образом, если больше не нажимать клавишу СТРЕЛКА ВВЕРХ, то наш корабль прекратит двигаться.
Затем мы добавляем значения movex и movey к значениям свойств playerObject.x и playerObject.y. Это дает нам имитацию инерции. Значения х и у объекта будут меняться в каждом временном интервале, независимо от того нажата клавиша или нет.

Также мы пересчитываем актуальные значения переменных centerx и centery для объекта playerObject (это координаты центра нашего объекта). Это актуальное значение нам потребуется для проверки на столкновение и правильного размещения взрывов.

Затем мы сравниваем значения centerx и centery c stage.width и stage.height. Если centerx больше чем ширина stage, то мы помещаем корабль на другую сторону экрана с координатой х равной –spriteWidth и наоборот. Аналогичную проверку делаем с centery.

		private function drawBackground():void {
canvasBD.copyPixels(backgroundBD,backgroundRect, backgroundPoint);
}

private function drawPlayer():void {
playerPoint.x=playerObject.x;
playerPoint.y=playerObject.y;
canvasBD.copyPixels(aShipAnimation[playerObject.arrayIndex],playerRect, playerPoint);
}
}
}


В каждом временном интервале нам нужно перерисовать фон. Это делается с помощью метода copyPixels() класса BitmapData. Мы копируем все пиксели из битмапдаты backgroundBD в битмапдату холста canvasBD. Значения backgroundBD, backgroundRect, backgroundPoint постоянны для каждого временного интервала и определены ранее.
Метод drawPlayer() немного сложнее. Нам нужно сначала приравнять значения playerObject.x и playerObject.y к значениям координат верхнего левого угла, с которого начнется копирование пикселей в битмапдату холста. Когда мы это сделаем, мы можем начать операцию копирования. На сей раз мы определяем текущий кадр анимации в массиве aShipAnimation, используя значение playerObject.arrayIndex. Также используем предрасчетные значения для переменных playerRect, playerPoint.

Если все сделали правильно, то получим следующий результат.

[SWF]http://coolisee.com/wordpress/wp-content/uploads/2010/07/31/7800asteroids.swf, 400, 400[/SWF]




Создание игры «Asteroids». Часть 2



В этой части мы посмотрим, как кэшировать анимацию для наших астероидов из библиотечного объекта с покадровой анимацией. Мы добавим астероиды в наш игровой цикл и посмотрим как их анимировать на экране, используя блитирование их изображений в наш единственный отображаемый объект – холст.
Для анимации астероидов я создал последовательность из 12 png файлов, которая выглядит следующим образом (см рисунок 5)

Анимация астероида
Рисунок 5. – Анимация астероида

Как видите изображение помещено в клип rock с координатами 0,0. Чтобы иметь возможность кэшировать этот клип, мы помещаем его в мувиклип-контейнер и называем rockToCache. Stage этого клипа выглядит следующим образом:

Stage клипа rockToCache
Размер 6. – Stage клипа rockToCache

Клип rockToCache содержит в себе экземпляр клипа rock, установленный в точке 0,0 и имеющий размер 36х32 пикселей. В поле «Instance name» присвоим ему имя RockToCach.
Теперь в коде нам нужно кэшировать анимацию этого контейнера и добавить астероиды в игровой цикл.
В нашем классе Main я добавил следующие переменные

		//** part 2 variables
var aAsteroidAnimation:Array=[];
var asteroidAnimationTimer:Timer;
var asteroidHolder:RockToCache;
var asteroidFrames:int=12;
var aRock:Array=[];
var level:int=1;
var rockPoint:Point=new Point(0,0);
var rockRect:Rectangle=new Rectangle(0,0,36,36);
var levelRocksCreated:Boolean=false;


aAsteroidAnimation – это массив, который содержит 12 экземпляров BitmapData, которые представляют собой изображения 12 кадров клипа астероида.
asteroidAnimationTimer – экземпляр класса Timer.
asteroidHolder – экземпляр библиотечного символа RockToCache.
aRock – это массив, который содержит все экземпляры астероидов, которые будут использоваться в игре.
level – целое число, которое представляет собой текущий уровень игры. В конечном счете, когда мы уничтожим все астероиды мы перейдем на новый уровень.
rockPoint – точка, которая будет использоваться в операции копирования copyPixels() для всех астероидов на холст canvasBD. Эта точка будет менять свои x и у на координаты того астероида, который будет отрисовываться.
rockRect – прямоугольник, ограничивающий область астероида (36х36).
levelRocksCreated – булево значение, в начале игры, когда астероиды не созданы ее значение равно false.

		private function animationTimerCompleteHandler(e:TimerEvent):void {
createAsteroidAnimation();
//startGame();
}


Теперь изменим метод animationTimerCompleteHandler. Закомментируем вызов метода startGame() и добавим вызов метода createAsteroidAnimation().

		private function createAsteroidAnimation():void {
animationCounter=1; // match frame on timeline
asteroidHolder=new RockToCache();
asteroidHolder.x=100;
asteroidHolder.y=100;
addChild(asteroidHolder);
asteroidAnimationTimer=new Timer(1,asteroidFrames);
asteroidAnimationTimer.addEventListener(TimerEvent.TIMER, cacheAsteroid);
asteroidAnimationTimer.addEventListener(TimerEvent.TIMER_COMPLETE, asteroidAnimationTimerCompleteHandler);
asteroidAnimationTimer.start();
}


Метод createAsteroidAnimation() создает основанный на таймере цикл, который обходит каждый кадр библиотечного клипа «ship» и кэширует его пиксели из кадра клипа в экземпляр BitmapData. В первой части мы сохраняли все повороты клипа корабля используя свойство rotate его контейнера. Для астероидов мы поступим по-другому. В каждой итерации мы вызовем метод gotoAndStop(), перейдем на следующий кадр и перерисуем его пиксели в экземпляр BitmapData и затем добавим этот экземпляр в массив aAsteroidAnimation. Я сделал так, потому что последовательность кадров в мувике – это наиболее вероятное, с чем вам придется иметь дело. Далее мы рассмотрим еще один метод создания массива битмапдат, когда будем использовать лист спрайтов для анимации ракет, выпускаемых нашим кораблем.

animationCounter=1; - устанавливаем начальный кадр анимации.
asteroidHolder=new RockToCache(); - создаем контейнер, содержащий клип астероида.
asteroidHolder.x=100;
asteroidHolder.y=100;
addChild(asteroidHolder); - устанавливаем его координаты и добавляем в список отображения.
asteroidAnimationTimer=new Timer(1,asteroidFrames);
asteroidAnimationTimer.addEventListener(TimerEvent.TIMER, cacheAsteroid);
asteroidAnimationTimer.addEventListener(TimerEvent.TIMER_COMPLETE, asteroidAnimationTimerCompleteHandler);
asteroidAnimationTimer.start(); - эти четыре строчки создают таймер с 12 итерациями. В каждой итерации будет вызван метода casheAsteroid() и когда пройдут все 12 итераций – метод asteroidAnimationTimerCompleteHandler(). Последняя строчка запускает этот таймер.

		private function cacheAsteroid(e:TimerEvent):void {
asteroidHolder.clip.gotoAndStop(animationCounter);
var spriteMapBitmap:BitmapData=new BitmapData(36,36,true,0x00000000);
spriteMapBitmap.draw(asteroidHolder,new Matrix());
aAsteroidAnimation.push(spriteMapBitmap);
//trace("caching " + animationCounter);
animationCounter++;

}

private function asteroidAnimationTimerCompleteHandler(e:TimerEvent):void {
trace("cacheAsteroid 7");
startGame();
}


Метод cacheAsteroid() вызывается 12 раз и в каждом вызове отрисовывает определенный кадр клипа rock в новый экземпляр BitmapData(). Затем этот объект добавляется в массив aAsteroidAnimation.

asteroidHolder.clip.gotoAndStop(animationCounter); - здесь мы переходим на кадр под номером, который храниться в переменной animationCounter.
var spriteMapBitmap:BitmapData=new BitmapData(36,36,true,0x00000000); - здесь создаем новую битмапдату для хранения изображения текущего кадра астероида.
spriteMapBitmap.draw(asteroidHolder,new Matrix()); - здесь отрисовываем кадр в битмапдату.
aAsteroidAnimation.push(spriteMapBitmap);
animationCounter++; - здесь добавляем эту битмапдату в массив aAsteroidAnimation и увеличиваем номер кадра на единицу, что в следующей итерации даст нам следующий кадр.

Метод asteroidAnimationTimerCompleteHandler() начинает нашу игру вызывая внутри себя метод startGame().

		private function runGame(e:TimerEvent) {
checkKeys();
updatePlayer();
updateRocks();
drawBackground();
drawPlayer();
drawRocks();
}


Метод runGame() изменился по сравнению с первой частью. Мы добавили вызовы новых методов. Метод updateRocks() выполняет всю работу по созданию астероидов в начале игры и обновлении их состояния в процессе игры, используя при этом их индивидуальные значения dx, dy и скорости.

		private function updateRocks():void {
if (!levelRocksCreated) {
for (ctr=0; ctr tempRock=new Object();
randInt=int(Math.random()*36);
randInt2=int(Math.random()*asteroidFrames);
tempRock.dx=aRotation[randInt].dx;
tempRock.dy=aRotation[randInt].dy;
tempRock.x=20;
tempRock.y=20;
tempRock.animationIndex=randInt2;
tempRock.bitmapData=aAsteroidAnimation[tempRock.animationIndex];
tempRock.frameDelay=3;
tempRock.frameCount=0;
tempRock.speed = (Math.random()*level)+1;
aRock.push(tempRock);
}
levelRocksCreated=true;
}
for each (tempRock in aRock) {
tempRock.x+=tempRock.dx*tempRock.speed;
tempRock.y+=tempRock.dy*tempRock.speed;

if (tempRock.x > stage.width) {
tempRock.x=0;
}else if (tempRock.x < 0) {
tempRock.x=stage.width;
}

if (tempRock.y > stage.height) {
tempRock.y=0;
}else if (tempRock.y < 0) {
tempRock.y=stage.height
}
}
}


Этот метод заботится о двух режимах работы для астероидов. В настоящее время, первое что он делает это создание астероидов для уровня. Как только он их создаст переменной levelRocksCreated присваивается значение true.

Другая важная задача для этого метода – обход всех астероидов в каждом игровом цикле и обновление их свойств х и у.

Итак, если levelRocksCreated == false то начинаем процесс создания астероидов для уровня. Я выбрал 3 астероида для первого уровня и в каждом новом уровне будем увеличивать это количество на единицу.

var tempRock:Object=new Object();
var randInt:int=int(Math.random()*36);
var randInt2:int=int(Math.random()*asteroidFrames);
tempRock.dx=aRotation[randInt].dx;
tempRock.dy=aRotation[randInt].dy;
tempRock.animationIndex=randInt2;


Здесь мы создаем объект для хранения астероида. Затем создаем случайное число от 0 до 35. Мы будем использовать это число для выбора случайных dx и dy из массива aRotation для этого астероида. Мы также выбираем случайный кадр анимации, чтобы все астероиды вращались асинхронно.

tempRock.x=20;
tempRock.y=20;
tempRock.frameDelay=3;
tempRock.frameCount=0;
tempRock.speed = (Math.random()*level)+1;
trace("tempRock.speed=" + tempRock.speed);
aRock.push(tempRock);


Эта часть кода настраивает новый астероид. Сначала мы задаем ему координаты (20,20), затем устанавливаем промежуток между кадрами равный 3, чтобы наши астероиды не крутились слишком быстро. Затем присваиваем случайную скорость, которая будет тем выше, чем выше уровень на котором находиться игрок. И наконец, добавляем астероид в массив aRock.

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

		private function drawRocks() {
var rockLen:int=aRock.length-1;
var ctr:int;

for each (var tempRock:Object in aRock) {

rockPoint.x=tempRock.x;
rockPoint.y=tempRock.y;

canvasBD.copyPixels(aAsteroidAnimation[tempRock.animationIndex],rockRect, rockPoint);

tempRock.frameCount++;
if (tempRock.frameCount > tempRock.frameDelay){

tempRock.animationIndex++;
if (tempRock.animationIndex > asteroidFrames-1) {
tempRock.animationIndex = 0;
}
tempRock.frameCount=0;
}
}

}


Последний метод отрисовывает астероиды в canvasBD. Здесь все аналогично отрисовке корабля.

[SWF]http://coolisee.com/wordpress/wp-content/uploads/2010/07/31/7800asteroids2.swf, 400, 400[/SWF]




Создание игры «Asteroids». Часть 3



В этой части мы рассмотрим стрельбу ракетами из нашего корабля. Мы будем использовать анимацию состоящую из BitmapData сохраненных из листа спрайтов. Мы сохраним каждый кадр листа в массив и будем проигрывать их, когда ракета будет запущена. Мы также добавим код для захвата нажатия клавиши [z] для запуска ракет и реализуем метод для ограничения количества ракет, которые игрок может выпустить за определенный промежуток времени.

Для создания листа со спрайтами ракетами я использовал Fireworks. В нем я создал новое изображение размером 40х4 пикселей. Я планирую получить анимацию из 10 кадров размером 4х4. В каждом кадре цвет моей ракеты отличается от предыдущего. Я сохранил это изображение в png и импортировал его в fla-файл.
Лист спрайтов при 800% увеличении выглядит так:

Лист спрайтов ракеты
Рисунок 7. – Лист спрайтов ракеты

В библиотеке fla-файла я в настройках этого изображения установил экспорт для ActionScript и в поле класса прописал missile_sheet. Это все настройки, которые нам понадобится сделать внутри fla-файла. Теперь в коде надо получить ракеты на экране.
В классе Main я добавил следующий импорт:

import flash.utils.getTimer


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

В разделе объявления переменных я добавил переменные, которые понадобятся для анимации ракет на экране

		var missleTileSheet:BitmapData;
var aMissileAnimation:Array;
var aMissile:Array; //holds missile objects fires
var missileSpeed:int=4;
var missileWidth:int=4;
var missileHeight:int=4;
var missileArrayLength=10;
var missilePoint:Point=new Point(0,0);
var missileRect:Rectangle=new Rectangle(0,0,4,4);
var missileMaxLife:int=50;
var missileFireDelay:Number=100;
var lastMissileShot:Number=getTimer();


missleTileSheet – это BitmapData, которая хранит изображение экземпляра missile_sheet из нашей библиотеки.
aMissileAnimation – массив, в котором хранятся 10 кадров анимации нашей ракеты. Все кадры представлены объектами BitmapData.
aMissile – массив, в котором хранятся все ракеты, которые выпустил корабль и которые находятся на экране.
missileSpeed – скорость ракеты составляет 4 пикселя за один игровой цикл.

Значения missileWidth и missileHeight также составляют 4 пикселя. Нет никаких оснований делать размеры ракеты и ее скорость одинаковыми. Скорость может быть какой угодно, но нужно помнить, что чем выше скорость ракеты, тем больше вероятность, что обнаружение столкновения ее с астероидом может быть пропущено.

Теперь мы создаем переменную missileArrayLength которая показывает максимальную длину массива с ракетами. Переменные missilePoint и missileRect используются для блитирования ракет в битмапдату canvasBD по аналогии с астероидами.

Последние три переменные определяют время жизни ракеты и количество ракет, которые могут находиться на экране одновременно. Изменяя переменную missileMaxLife, вы измените количество игровых циклов, в которых ракета будет находиться на экране. Изменяя missileFireDelay на более низкое значение, вы позволите большему количеству ракет быть запущенными в более короткий промежуток времени. Последняя переменная – значение, возвращаемое методом getTimer(), которая хранит время, когда была запущена последняя ракета. Смысл этой переменной я объясню позднее.

В методе createObjects() появились новые строки для создания листа спрайтов ракеты и сохранения 10 кадров в массив битмапдат.

			//part 3 init tilesheet for missiles
aMissile=[];
aMissileAnimation=[];
missleTileSheet=new missile_sheet(40,4);
var tilesPerRow:int=10;
var tileSize:int=4;
for (var tileNum=0;tileNum<10;tileNum++) {
var sourceX:int=(tileNum % tilesPerRow)*tileSize;
var sourceY:int=(int(tileNum/tilesPerRow))*tileSize;
var tileBitmapData:BitmapData=new BitmapData(tileSize,tileSize,true,0x00000000);
tileBitmapData.copyPixels(missleTileSheet,new Rectangle(sourceX,sourceY,tileSize,tileSize),new Point(0,0));
aMissileAnimation.push(tileBitmapData);
}


Во-первых, мы инициализируем массив aMissile. В нем будут хранится все ракеты, которые были выпущены игроком, таким образом мы можем обновлять их положение на экране и проверять их на столкновение с астероидами. Затем мы инициализируем переменную aMissileAnimation. Это массив, в котором будут храниться 10 кадров анимации нашей ракеты, представленные экземплярами BitmapData. Затем мы создаем экземпляр missile_sheet из нашей библиотеки. Т.к. png при внедрении в библиотеку получает родительский класс BitmapData, то экземпляр missile_sheet мы можем спокойно присвоить переменной missileTileSheet. При создании missile_sheet мы используем в конструкторе значения его ширины и высоты.

Наша следующая задача – в цикле обойти все 10 спрайтов в листе и создать BitmapData для каждого из них. Сначала мы устанавливаем переменной tilesPerRow значение, равное количеству спрайтов в строке (40/4=10). Переменная tileSize будет равна 4, т.к. один спрайт имеет размер 4х4 пикселя. Если бы ширина и высота спрайта не равнялась бы друг другу, то нам нужно было бы создать набор переменных, в которых хранились бы значения ширины и высоты для каждого спрайта. Я создал квадратные спрайты, чтобы избежать этой работы. Теперь создаем цикл с 10 итерациями.

В каждой итерации сначала нам нужно найти координату х верхнего левого угла спрайта в листе, с которого мы начнем операцию копирования пикселей. Это значение можно получить, умножая остаток tileNum/tilesPerRow на значение размера спрайта. Так для первого прохода значения х и у будут равны нулю. В следующей строчке мы создаем новый экземпляр BitmapData и называем его tileBitmapData, размер этой битмапдаты будет 4х4 и она будет поддерживать прозрачность. Затем мы выполняем метод copyPixels() для копирования пикселей из missileTileSheet в tileBimapData. Нам для этого понадобятся прямоугольник ограничивающий область копирования и точка, определяющая координаты верхнего левого угла этой области. Эта точка всегда имеет координаты (0,0) а прямоугольник будет размером 4х4 пикселя и координаты его левого угла будут определяться значениями source и sourceY. Затем мы помещаем полученную битмапдату в массив aMissileAnimation.

		private function runGame(e:TimerEvent) {
checkKeys();
updatePlayer();
updateRocks();
updateMissiles();
drawBackground();
drawPlayer();
drawRocks();
drawMissiles();
}


В методе runGames() добавились вызовы новых функций, которые обновляют положение ракет и отрисовывают их. В методе checkKeys() добавлена возможность захвата нажатий клавиши [z] и вызов функции стрельбы при ее нажатии.

		private function fireMissile():void {

if (getTimer() > lastMissileShot + missileFireDelay) {
var tempMissile:Object=new Object();
tempMissile.x=playerObject.centerx;
tempMissile.y=playerObject.centery;
tempMissile.dx=aRotation[playerObject.arrayIndex].dx;
tempMissile.dy=aRotation[playerObject.arrayIndex].dy;
tempMissile.speed=missileSpeed;
tempMissile.life=50;
tempMissile.lifeCount=0;
tempMissile.animationIndex=0;
aMissile.push(tempMissile);
lastMissileShot=getTimer();
}
}


Когда нажата клавиша [z] сначала мы проверяем значение переменной lastMissileShot. Помним, что в объявлении переменных мы задали ей значение getTimer(). Перед тем, как ракета будет выпущена первый раз, она хранит количество миллисекунд прошедших после ее инициализации. Если значение getTimer() больше чем выражение lastMissileShot + missileFireDelay, значит прошло достаточно времени, чтобы запустить новую ракету.

Итак первое, что мы делаем – создание объекта tempMissile для хранения всех свойств ракеты. Затем мы присваиваем координатам ракеты значения координат центра корабля, тогда будет складываться впечатление, что ракеты запускаются из центра корабля. Затем мы приравниваем dx и dy из массива aRotation значениям соответствующим свойств ракеты.

Теперь мы устанавливаем значение life = 50, т.е. наша ракета будет жить в течении 50 игровых циклов, а затем она будет удалена с экрана. animationIndex = 0 – анимация ракеты начнется с первого спрайта из нашего листа. Этот индекс увеличивается в каждой игровом цикле. Затем мы добавляем ракету в массив aMissile и присваиваем переменной lastMissileShot новое значение.

		private function updateMissiles():void {
var missileLen:int=aMissile.length-1;
for (var ctr:int=missileLen;ctr>=0;ctr--) {
var tempMissile:Object=aMissile[ctr];
tempMissile.x+=tempMissile.dx*tempMissile.speed;;
tempMissile.y+=tempMissile.dy*tempMissile.speed;

if (tempMissile.x > stage.width) {
tempMissile.x=0;
}else if (tempMissile.x < 0) {
tempMissile.x=stage.width;
}

if (tempMissile.y > stage.height) {
tempMissile.y=0;
}else if (tempMissile.y < 0) {
tempMissile.y=stage.height
}

tempMissile.lifeCount++;
if (tempMissile.lifeCount > tempMissile.life) {
aMissile.splice(ctr,1);
tempMissile=null;
}

}
}


Метод updateMissile() вызывается в каждом игровом цикле. Этот метод практически идентичен методу updateRock(), но имеет одно важное отличие. Ракеты нужно удалять с экрана, когда пройдет 50 игровых циклов с момента их запуска. Поэтому мы не можем использовать оператор for each для обхода массива ракет, т.к. время жизни у них разное. В цикле мы обходим каждую ракету, обновляем ее позицию, проверяем на положение относительно краев сцены, и удаляем со сцены и из массива aMissile если время жизни ракеты составляет больше 50 циклов.

Последнее изменение в коде в этой части:

		private function drawMissiles():void {
var missileLen:int=aMissile.length-1;

for each (var tempMissile:Object in aMissile) {

missilePoint.x=tempMissile.x;
missilePoint.y=tempMissile.y;

canvasBD.copyPixels(aMissileAnimation[tempMissile.animationIndex],missileRect, missilePoint);

tempMissile.animationIndex++;
if (tempMissile.animationIndex > missileArrayLength-1) {
tempMissile.animationIndex = 0;
}
}
}


Здесь все тоже самое, что и в методе drawRock(), поэтому не будем останавливаться на этом методе подробно.

Вот что мы имеем к концу этой части. Используйте клавишу [z] для стрельбы.

[SWF]http://coolisee.com/wordpress/wp-content/uploads/2010/07/31/7800asteroids3.swf, 400, 400[/SWF]





Создание игры «Asteroids». Часть 4



В этой части мы добавим проверку на столкновения и простой счетчик производительности.

Экземпляры BitmapData представляют собой совокупность цветных и прозрачных пикселей. Когда два экземпляра BitmapData накладываются друг на друга и какие-либо непрозрачные пиксели будут в области наложения с той и другой стороны, то будет зарегистрировано столкновение. Это сильно отличается от стандартной проверки двух прямоугольных областей. Также можно проверить на столкновение экземпляры BitmapData с экземплярами Sprite и MovieClip, но мы в этом примере будем использовать простую проверку двух битмапдат.

Нужно усвоить, что когда вы создаете объект BitmapData из другого объекта BitmapData с помощью оператора присваивания (=), вы просто создаете ссылку на первоначальный объект. У вас нет двух различных экземпляров BitmapData, а есть две ссылки на один и тот же экземпляр. Это важно, потому что наша игра составлена из большого количества таких объектов. Если мы захотим изменить один из кадров анимации астероида, то все астероиды получат тоже самое изменение, т.к. ссылаются на один и тот же массив с битмапдатами. Возможно, появится мысль, что если эти битмапдаты для всех одинаковы, то проверить их на столкновение не получиться, но это не так. Как вы помните из предыдущих частей, в каждом игровом цикле, анимация получается путем замены одной битмапдаты из массива на следующую, из того же массива. Каждый астероида хранит значения индекса той битмапдаты в массиве. Нет способа, чтобы использовать этот индекс для определения столкновений. Мы должны создать свойство bitmapData для каждого объекта. Эта переменная будет хранить ссылку на текущий объект BitmapData. Если вы работали с классом BitmapData то вам может показаться, что теперь нужно использовать метод clone(), чтобы создать новую битмапдату из текущей и проверять на столкновения новые битмапдаты, но в этом нет необходимости. Это не логично, но это работает.

Новый метод checkCollisions представлен ниже. Я не буду объяснять каждую строчку, но на важных особенностях мы остановимся подробнее.

		private function checkCollisions():void {
missileLen=aMissile.length-1;
rockLen=aRock.length-1;

rocks: for (rockCtr=rockLen;rockCtr>=0;rockCtr--) {
tempRock=aRock[rockCtr];
rockPoint.x=tempRock.x;
rockPoint.y=tempRock.y;
missiles: for (missileCtr=missileLen;missileCtr>=0;missileCtr--) {
tempMissile=aMissile[missileCtr];
missilePoint.x=tempMissile.x;
missilePoint.y=tempMissile.y;
//trace("1");
try{
if (tempMissile.bitmapData.hitTest(missilePoint,255,tempRock.bitmapData,rockPoint,255)) {
// trace("hit!");
createExplode(tempRock.x+18,tempRock.y+18);
tempMissile=null;
tempRock=null;
aMissile.splice(missileCtr,1);
aRock.splice(rockCtr,1);
break rocks;
break missiles;

}
}catch(e:Error) {
trace("error in missle test");
}
}

//trace("2");
playerPoint.x=playerObject.x;
playerPoint.y=playerObject.y;
try{
if (tempRock.bitmapData.hitTest(rockPoint,255,playerObject.bitmapData,playerPoint,255)){
//trace("ship hit");
createExplode(tempRock.x+18,tempRock.y+18);
tempRock=null;
aRock.splice(rockCtr,1);
}
}catch(e:Error) {
trace("error in ship test");
}
}

//trace("3");
}


Метки

Мы должны обойти в цикле все астероиды и ракеты, чтобы проверить, не столкнулись ли они друг с другом. Нам не нужно проверять столкнулись ли две ракеты или два астероида, но нам нужно проверить столкновения астероидов и корабля. По этой причине астероиды мы обходим во внешнем цикле, а ракеты во внутреннем. Если бы мы обходили ракеты во внешнем цикле, то для каждой ракеты мы бы проверили столкновение с каждым астероидом и стал бы нужен еще цикл для проверки каждого астероида с кораблем. Это пустая трата времени, поэтому мы проходим через каждый астероид и проверяем сначала на столкновения с каждой ракетой и ЕСЛИ НЕТ, то проверяем на столкновение с кораблем. Для этого мы установили две метки:

rocks:
missiles:


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

Выполнение цикла сверху вниз

Мы используем i—вместо i++ в циклах обхода астероидов и ракет, потому что нам нужно вырезать (splice) столкнувшиеся объекты из соответствующих массивов, когда их столкновение будет обнаружено. Если бы мы шли снизу и нам нужно было бы удалить 10-й элемент в массиве ракет, произошла бы ошибка. Мы бы пропустили проверку 11-го элемента в нашем массиве, который опуститься в массиве на место десятого.

Метод BitmapData.hitTest()

if (tempMissile.bitmapData.hitTest(missilePoint,255,tempRock.bitmapData,rockPoint,255)) {


Метод hitTest() класса BitmapData очень эффективен. Когда мы проверяем экземпляр BitmapData этот метод просматривает все пиксели объекта, чтобы проверить не перекрываются ли они с пикселями другого объекта. Если эти пиксели непрозрачны (или не превышают заданного порога прозрачности) то столкновение регистрируется. Я создал две переменные rockPoint и missilePoint на уровне класса, которые представляют собой координаты левого верхнего угла проверяемого объекта в глобальных координатах. 255 – значение альфа-канала пикселя для которого будет осуществляться проверка на столкновение.

tempMissile.bitmapData и tempRock.bitmapData хранят ссылки на битмапдаты, которые будем проверять. В каждой итерации они изменяются.


Ниже я добавил в метод drawMissiles() следующую строчку:

tempMissile.bitmapData=aMissileAnimation[tempMissile.animationIndex];


Поскольку мы обновляем ракету на экране, мы также изменяем свойство tempMissile.bitmapData в которой хранится BitmapData текущего «кадра» анимации. Подобную строку мы добавим и в метод drawRocks().

tempRock.bitmapData=aAsteroidAnimation[tempRock.animationIndex];


и в метод checkKeys() в блоки захвата клавиш СТРЕЛКА ВЛЕВО и СТРЕЛКА ВПРАВО

playerObject.bitmapData=aShipAnimation[playerObject.arrayIndex];


Новый метод createExplode(), который вызывается когда обнаружено столкновение ракеты с астероидом, имеет два параметра – значения координат х и у. Это координаты места откуда наш взрыв появиться и его частицы будут распротраняться. Понятно, что tempRock.x+18,tempRock.y+18 ссылаются на центр астероида.

Т.к. частицы взрыва – это просто художественное оформление игры, которое ни с чем не взаимодействует, я решил ограничить их количество, которое доступно для отображения в определенный промежуток времени. Я сделал это путем реализации пула (pool) частиц. В начале игры я создаю массив из 500 частиц и называю его aFarmParticle. Вот код:

		private function createFarmParticles() {
var particleCtr:int;
for (particleCtr=0;particleCtr var tempPart:Object={};
tempPart.lifeCount=0;
tempPart.life=0;
tempPart.x=0;
tempPart.y=0;
tempPart.dx=0;
tempPart.dy=0;
tempPart.speed=0;
tempPart.bitmapData=null;
aFarmParticle.push(tempPart);
}
}


Переменной maxParticles я присвоил значение 500. Этот код выполняет 500 раз цикл, в котором каждый раз создается частица и помещается в массиве aFarmParticle для более позднего использования.

Когда необходимо создать взрыв, мы вызываем метод createExplode(). Этот метод с помощью цикла с количеством итераций maxPartsPerExplode (сейчас это значение равно 40) перемещает частицы из массива aFarmParticle в массив aActiveParticle.

		private function createExplode(xval:Number,yval:Number):void {

for (explodeCtr=0;explodeCtr farmLen=aFarmParticle.length-1;
if (farmLen >0){
tempPart=aFarmParticle[farmLen];
aFarmParticle.splice(farmLen,1);
tempPart.lifeCount=0;
tempPart.life=int(Math.random()*partMaxLife)+partMinLife;;
tempPart.x=xval;
tempPart.y=yval;
tempPart.speed=(Math.random()*partMaxSpeed)+1;
randIntFrame=int(Math.random()*10);
tempPart.bitmapData=aMissileAnimation[randIntFrame];
randIntVector=int(Math.random()*36);
tempPart.dx=aRotation[randIntVector].dx;
tempPart.dy=aRotation[randIntVector].dy;
aActiveParticle.push(tempPart);
}
}
}


В каждой итерации мы сначала проверяем не равна ли длина массива пула нулю, что гарантирует наличие частиц для перемещения в массив aActiveParticle. Мы создаем переменную tempPart, в которой будет храниться последняя частица из массива aFarmParticle. Затем вырезаем эту частицу из этого массива, задаем ей свойства необходимые для перемещения по экрану и добавляем ее в массив aActiveParticle

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

		private function drawParts():void {

activeLen=aActiveParticle.length-1;

for (partCtr=activeLen;partCtr>=0;partCtr--) {
removePart=false;
tempPart=aActiveParticle[partCtr];
tempPart.x+=tempPart.dx*tempPart.speed;;
tempPart.y+=tempPart.dy*tempPart.speed;

if ((tempPart.x > stage.width) || (tempPart.x < 0)) {
removePart=true;
}

if ((tempPart.y > stage.height) || (tempPart.y < 0)){
removePart=true;
}

tempPart.lifeCount++;
if (tempPart.lifeCount > tempPart.life) {

}

if (removePart) {
aFarmParticle.push(tempPart);
aActiveParticle.splice(partCtr,1);

}else{
partPoint.x=tempPart.x;
partPoint.y=tempPart.y;

canvasBD.copyPixels(tempPart.bitmapData,partRect, partPoint);

}
}
}


В этом методе частица удаляется с экрана, как только время ее жизни превышает установленное и когда она выходит за границы экрана. Когда частица удаляется, она удаляется из массива aActiveParticle и добавляется в массив aFarmParticle.

Код игрового цикла теперь выглядит так:

		private function runGame(e:TimerEvent) {

checkKeys();
updatePlayer();
updateRocks();
updateMissiles();
checkCollisions();
drawBackground();
drawPlayer();
drawRocks();
drawMissiles();
drawParts();
frameTimer.countFrames();
frameTimer.render();
if (aRock.length ==0 && aActiveParticle.length==0) stopRunningGame();
}


Также я добавил простой счетчик кадров и оболочку для запуска игры. Этот код и окончательный код класса Main вы найдете в исходниках.
На этом у меня все :).

Исходники

2 комментария:

  1. Отличный перевод, спасибо!
    Автор освещает тему кэширования анимации (астероиды). И кэширования поворотов статичного изображения (корабль). А вот как бы он кэшировал и то и другое ВМЕСТЕ? Если бы у корабля был мощный, красиво отрисованный, хвостовой пламень? Как тогда кэшировать? Анимация пламеня, допустим, 36 кадров. На каждый кадр: 360 поворото-картинок кэша? 36*360=12960 битмапдат. Вся память кончится. А если корабль большой (200х100), а не такая крохотуля как у автора? Память кончится гораздо быстрее...
    Как выходить из таких ситуаций? Не кэшировать пламень? Поворачивать вручную его Битмапу каждый раз и блитить?

    ОтветитьУдалить
  2. Если игра не динамическая, т.е. не много объектов, то может и с блитированием не стоит заморачиваться (если у вас такие размеры большие, то видимо объектов не много), если нет то мое имхо такое:
    - 360 поворото-картинок слишком много, я думаю 36 вполне хватит, если картинка пламени гдето 100*100, на пиксель выделяется 4 байта - 1 картинка 40 кб, вся анимация 40 мб (приблизительно)
    - поворот корабля - 36 * 20000 * 4 = 3 мб (приблизительно)
    в принципе не так много
    вообще да, нужно понимать, что должен быть определенный баланс между потребляемой производительностью и памятью (в этом случае перекос в потребляемую память), может кое-чего можно и просто повращать и каждый раз блитировать.
    тут я думаю общего правила нет и нужно смотреть каждый проект индивидуально

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