Найти тему
HD4E Games

Java Script. Космическая стрелялка. Туториал. Часть 3.

Оглавление

<Первая часть — Java Script. Космическая стрелялка. Туториал. Часть 1. ©HD4E>

<Вторая часть — Java Script. Космическая стрелялка. Туториал. Часть 2. ©HD4E>

7.Добавление динамического заднего фона.

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

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

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

Загрузите это изображение, как обычно в папку images, назвав его back.png. Также добавьте код загрузки к остальному коду в функции load:

wade.loadImage("images/back.png");

(c)HD4E>)

Добавим в функцию init следующий код:

// создаем новый спрайт, с нашим изображением
var backSprite = new Sprite("images/back.png");
// устанавливаем размер спрайта равный ширине и высоте текущей области прорисовки
backSprite.setSize(wade.getScreenWidth(), wade.getScreenHeight());
// создаём новый объект с нашим спрайтом
var backObject = new SceneObject(backSprite);
// добавляем объект на сцену
wade.addSceneObject(backObject);

Теперь давайте добавим немного звезд поверх нашего заднего фона. Я буду использовать один крошечный спрайт(16x16) изображение которого я размещу в нашей папке images, назвав его star.png. Затем я загружу его как обычно:

wade.loadImage("images/star.png");

И в функции init я создам немного этих звезд в случайных позициях, с случайной ориентацией и размером. Я поставлю их на тот же слой, на котором находится наш задний фон(10 слой) на данный момент, но мы вернемся к этому еще, но чуть позже. Код:

for(var i=0; i<15; i++)
{
// определяем случайный размер звезды между 8 и 16
var size = Math.random()*8+8;
// определяем случайное вращение между 0 и 6.28
var rotation = Math.random()*6.28;
// определяем случайную координату по оси X, на всей ширине области прорисовки
var posX = (Math.random()-0.5)*wade.getScreenWidth();
// определяем случайную координату по оси Y, на всей ширине области прорисовки
var posY = (Math.random()-0.5)*wade.getScreenHeight();
// создаем спрайт звезды со слоем номер 10
var starSprite = new Sprite("images/star.png", 10);
// устанавливаем размер спрайта
starSprite.setSize(size, size);
// создаем объект сцены с координатами posX и posY
var star = new SceneObject(starSprite, 0, posX, posY);
// устанавливаем вращение
star.setRotation(rotation);
// добавляем нашу звезду на сцену
wade.addSceneObject(star);
}

Теперь наша игра должна выглядеть немного получше.

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

// здесь мы заставляем звезду двигаться по заданным координатам
star.moveTo(posX, wade.getScreenHeight()/2 + size/2,20);
// здесь мы говорим что по окончанию движения, должен выполниться следующий далее код
star.onMoveComplete= function()
{
// получаем старый размер звезды из текущего спрайта
var size = this.getSprite(). getSize().y;
// создаем новую случайную позицию по оси X
var posX = (Math.random()-0.5)*wade.getScreenWidth();
// устанавливаем новую позицию
this.setPosition(posX, -wade.getScreenHeight()/2 - size/2);
// заставляем звезду двигаться
this.moveTo(posX, wade.getScreenHeight()/2+size/2, 20);
};

Это даёт впечатление, что мы постоянно двигаемся вперёд. И это является приятным штрихом.

8. Рейтинг.

Теперь давайте также добавим счетчик баллов. Мы сделаем его простым, так что это будет простой текстовый спрайт в верхнем правом углу экрана. Я определю пару переменных в верхней части нашей App функции:

var scoreCounter;
var score;

Затем в нашей функции init мы создадим объект для этой переменной. Я установлю выравнивание текста направо, и я размещу объект в верхнем правом углу экрана:

score = 0;
// создаем новый текстовый спрайт
var scoreSprite = new TextSprite(score.toString(), '32 px Verdana','#f88','right');
// создаём новый объект с этим спрайтом
scoreCounter = new SceneObject(scoreSprite, 0, wade.getScreenWidth()/2-10, wade.getScreenHeight()/2+30);
// добавляем объект на сцену
wade.addSceneObject(scoreCounter);

Теперь давайте наберем несколько очков, код для этого мы разместим там где мы уничтожаем вражеский корабль в нашем "главном цикле - fire", до того как мы удаляем вражеский корабль. Код:

score +=10;
scoreCounter.getSprite(). setText(score);

9. Смерть и возвращение обратно в главное меню.

Игра почти полностью функциональна, но тут отсутствует одна небольшая жизненно-важная деталь: мы должны умирать, когда ударяемся о вражеский корабль или о вражеский снаряд. Чтобы сделать это, нам прежде всего нужно знать какой объект является вражеским кораблём, а какой снарядом врага.

У вражеского корабля есть флаг - isEnemy, который мы можем проверить, таким образом мы можем добавить похожий флаг (isEnemyBullet) для снарядов врага, при их создании в функции enemyShip.fire (которая находится в функции this.spawnEnemy) :

bullet.isEnemyBullet = true;

(< После строчки :

var bullet = new SceneObject(sprite, 0, startX, startY);

(c)HD4E>)

Теперь мы создадим новый главный цикл(который мы назовем - die). Хорошее место для размещения, это наша функция init. В этом цикле мы видим, какие объекты перекрывают наш корабль и если любой из них имеет установленный флаг, либо isEnemy, либо isEnemyBullet, то мы создаём взрыв и удаляем корабль со сцены. Код:

wade.setMainLoop(function()
{
// получаем объекты, которые перекрывают наш корабль
var overlapping = ship.getOverlappingObjects();
// запускаем цикл перебора этих объектов
for(var i=0; i<overlapping.length; i++)
{
// проверяем столкнулись ли мы с врагом или снарядом
if(overlapping[i].isEnemy || overlapping[i]. isEnemyBullet)
{
// если да запускаем функцию взрыва, и передаем ей координаты
wade.app.explosion(ship.getPosition());
// удаляем наш корабль
wade.removeSceneObject(ship);
}
}
}, 'die');

Если вы попробуете запустить игру сейчас, то увидите, что этого не достаточно: даже когда корабль удален со сцены, мы всё ещё можем стрелять, и мы можем все ещё умереть и сгенерировать больше взрывов. Это потому, что наши циклы "fire" и "die" всё еще активны, и мы действительно должны удалить их когда мы умираем, с помощью вот этого кода:

wade.setMainLoop(null, 'fire');
wade.setMainLoop(null, 'die');

(< Вставьте этот код после удаления корабля в main loop - die (c)HD4E>)

Конечно это немного расстраивает, что когда мы умираем, чтобы сыграть снова, мы должны обновить страницу. Чтобы решить эту проблему, мы должны будем переместить некоторый код. Функция init в том состояние в котором она есть сейчас, делает много вещей:

  • Устанавливает параметры разрешения
  • Создаёт и инициализирует объекты
  • И запускает нашу главную петлю.

Давайте переместим некоторый код в новую функцию, которую назовём startGame. Код:

this.startGame = function()
{
var sprite = new Sprite('images/ship.png');
var mousePosition = wade.getMousePosition();
ship = new SceneObject(sprite, 0, mousePosition.x, mousePosition.y);
wade.addSceneObject(ship);
wade.setMainLoop(function()
{
var nextFireTime = lastFireTime + 1 / fireRate;
var time = wade.getAppTime();
if (wade.isMouseDown() && time >= nextFireTime)
{
lastFireTime = time;
var shipPosition = ship.getPosition();
var shipSize = ship.getSprite().getSize();
var sprite = new Sprite('images/bulletmini.png');
var bullet = new SceneObject(sprite, 0, shipPosition.x, shipPosition.y - shipSize.y / 2);
wade.addSceneObject(bullet);
activeBullets.push(bullet);
bullet.moveTo(shipPosition.x, -500, 600);
bullet.onMoveComplete = function()
{
wade.removeSceneObject(this);
};
}
for (var i=activeBullets.length-1; i>=0; i--)
{
var colliders = activeBullets[i].getOverlappingObjects();
for (var j=0; j < colliders.length; j++)
{
if (colliders[j].isEnemy)
{
var position = colliders[j].getPosition();
wade.app.explosion(position);
score += 10;
scoreCounter.getSprite().setText(score);
wade.removeSceneObject(colliders[j]);
wade.removeSceneObject(activeBullets[i]);
wade.removeObjectFromArrayByIndex(i, activeBullets);
break;
}
}
}
}, 'fire');
wade.setMainLoop(function()
{
var overlapping = ship.getOverlappingObjects();
for(var i=0; i<overlapping.length; i++)
{
if(overlapping[i].isEnemy || overlapping[i]. isEnemyBullet)
{
wade.app.explosion(ship.getPosition());
wade.removeSceneObject(ship);
wade.setMainLoop(null, 'fire');
wade.setMainLoop(null, 'die');
}
}
}, 'die');
score = 0;
var scoreSprite = new TextSprite(score.toString(), '32px Verdana','#f88','right');
scoreCounter = new SceneObject(scoreSprite, 0, wade.getScreenWidth()/2-10, -wade.getScreenHeight()/2+30);
wade.addSceneObject(scoreCounter);
enemyDelay = 2000;
nextEnemy = setTimeout(wade.app.spawnEnemy, enemyDelay);
};

Итак в фуекции init мы устанавливаем только ограничение разрешения и создаём задний фон со звёздами, а всё остальное сделано в startGame. Я делаю так, поскольку я бы хотел добавить экран меню перед началом игры. Этот экран меню будет с нашим задним фоном со звездами, с простым текстовым спрайтом, просящим пользователя нажать, для старта. Когда пользователь делает клик мы начинаем игру. Итак теперь мы можем добавить внизу нашей фуекции init, следующий код:

// создаем новый текстовый спрайт с нашей надписью
var clickText = new TextSprite('Нажмите чтобы начать', '32px Verdana', 'white', 'center');
// создаем новый объект с нашим спрайтом
var clickToStart = new SceneObject(clickText);
// добавляем наш объект на сцену
wade.addSceneObject(clickToStart);
// обрабатываем событие нажатия мышки
wade.app.onMouseDown = function()
{
// При нажатии мышки удаляем наш объект с надписью
wade.removeSceneObject(clickToStart);
// запускаем нашу функцию начала игры
wade.app.startGame();
// сбрасываем обработку нажатия мыши( в функции startGame у нас будет другой обработчик нажатия мыши)
wade.app.onMouseDown = 0;
};

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

Теперь когда игра заканчивается(т.е. когда игрок умирает) мы хотим уйти назад к экрану главного меню, и мы можем сделать это с помощью очистки сцены, и вызова функции init заново. Итак просто после удаления нашего корабля в нашей "главном цикле - die", мы можем сделать следущее:

// очистка сцены
wade.clearScene();
// новый запуск приложения с начальными настройками
wade.app.init();
// отмена появления врагов
clearTimeout(nextEnemy);

Обратите внимание, что я также отменяю появление следующего врага с помощью функции clearTimeout. Но хотя это работает, не очень приятно быть выгнаным из игры, как только ты умираешь ... мы можем подождать пару секунд, итак давайте переместим этот код внутрь функции setTimeout, где наша задержка будет 2 секунды:

setTimeout(function()
{
wade.clearScene();
wade.app.init();
clearTimeout(nextEnemy);
}, 2000);

Теперь всё намного лучше, и я думаю, что игра почти завершена.

10. Быстрая оптимизация.

Вероятно нам стоит потратить несколько минут на оптимизацию вещей, поскольку если вы попробуете игру такой какая она есть, на очень дешевом мобильном устройстве, она может быть немного медленной. Главная причина замедления на мобильных, в основном говоря, плохая виртуальная скорость заполнения. Это означает, что чем больше пикселей вы рисуете на вашем canvas, тем медленнее работает ваша игра. По этим причинам WADE пытается минимизировать количество пикселей, которые он рисует, так что например, если в области экрана нету изменений, он не перерисовывает его.

Однако учитывая что случилось на нашем слое с задним фоном: звезды двигаются и они должны быть перерисованы при каждом кадре. Но поскольку звезды являются полупрозрачными обьектами, любой обьект позади них, тоже должен быть перерисован в каждом кадре, и в нашем случае это большой черный прямоугольник заднего плана(который состоит из множества пикселей). Хотя если мы переместим его на его собственный слой, нам не нужно будет прорисовывать его каждый кадр, поскольку каждый слой является отдельным объектом canvas. Таким образом задний черный фон canvas никогда не изменится, и мы можем нарисовать его только один раз. Так давайте переместим наш спрайт заднего фона на 11 слой:

(< Изменим слой при создании спрайта заднего фона с 10 на 11 (c)HD4E>)

var backSprite = new Sprite("images/back.png",11);

11.Постоянный счет.

Сделав игру, у нас осталась одна последняя вещь, я бы хотел добавить к этой игре-примеру: постоянную доску рекордов. Я сказал "постоянную", но я не буду загружать рекорды на сервер; вместо этого, я сохраню их локально, но таким образом, что рекорды будут всё еще там в следующий раз когда пользователь перейдет на страницу игры с тем же браузерм. Конечно это в действительности не "постоянный' способ сохранения рекордов, поскольку наши пользователи всегда могут решить удалить кэш браузера и локально сохраненные файлы браузера(иногда они это делают) и в таком случае их рекорды будут потеряны. Но это все еще очень полезная техника для использования когда вы хотите сохранить не жизненно-важную информацию без необходимости прибегать к внешнему серверу для загрузки данных. Прежде всего, в нашей функции init, мы собираемся попытаться извлечь локально сохраненный объект названный shooterData. Этого объекта не существует когда мы в первый раз запускаем игру, но затем игра будет сохранять этот объект в локальном хранилище. Итак мы видим существует ли этот объект, и соответственно, устанавливаем переменныую рекорда.

// Извлекаем данные
var shooterData = wade.retrieveLocalObject('shooterData');
// Если shooterData существует, то достаем из него данные highScore, либо если объекта нету, то ставим 0
var highScore = (shooterData && shooterData.highScore) || 0;

(< Добавьте код выше, в функцию init (c)HD4E>)

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

(< Добавьте следующий код после создания обьекта с нашим основным текстом. (c)HD4E>)

clickToStart.addSprite(new TextSprite('Your best score is ' + highScore, '18px Verdana', 'yellow', 'center'), {y: 30});

И наконец то, когда мы умираем мы проверяем эту переменную снова, и если мы получаем доску рекордов, мы сохраняем новый рекорд в объекте shooterData.

(< Добавьте следующий код после удаления нашего корабля при смерти. (c)HD4E>)

var shooterData = wade.retrieveLocalObject('shooterData');
var highScore = (shooterData && shooterData.highScore) || 0;
// Если данный рекорд больше лучшего рекорда, то ...
if (score > highScore)
{
// Записываем в shooterData новый рекорд
shooterData = {highScore: score};
// Сохраняем данные shooterData
wade.storeLocalObject('shooterData', shooterData);
}

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

12.Исправление ошибок.

Если вы откроете консоль браузера в то время когда вы будете проверять игру (что является хорошей практикой), вы вероятно заметите что игра идет с ошибкой:

Uncaught TypeError: Cannot call method 'setPosition' of undefined.

Это случается поскольку, когда мы двигаем мышку, выполянется функция onMouseMove в нашего приложения, и в этой функции мы пытаемся установить позицию объекта корабля. Однако пока игра не стартовала, объект корабля не был еще определен - отсюда ошибка. Чтобы исправить это, мы можем изменить содержание нашей функции onMouseMove на это:

ship && ship.setPosition(eventData.screenPosition.x, eventData.screenPosition.y);

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

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

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

(c)HD4E>)