Создаем HTML5 игру. Часть 2: Игровая логика

15 февраля, 2014
6 минут(ы) чтения

Если контент не отображается, включите VPN.

Создание игры на HTML5. Часть вторая.
Создаем директории и файлы как показано на рисунке

В предыдущей части был разбор существующих HTML5 технологий предназначенных для создания браузерных игр. Теперь нам предстоит создать архитектуру игрового проекта, написать логику и работать с ресурсами. В этот раз вместо слов перейдём к коду. Крепитесь, его будет много.

Если вы что-то пропустили, вот ссылка на предыдущую часть статьи.

Первым делом создаём файл index.html. В нём подключаем игровые стили, require-модуль, контейнер с игрой, панель загрузки и ссылку для привязки событий через клавиатуру (можно привязывать события напрямую к контейнеру, только будьте уверены что ваши привязки событий не будут конфликтовать с фреймворком). Старайтесь придерживаться чистоты в вёрстке как в .html, так и в .css шаблонах. Главное правило — сократить дублирование кода. Следите чтобы созданные стили и наименование идентификаторов или классов не перекрывали используемые значения в шаблонах.

Шаблон

INDEX.HTML

<!DOCTYPE html>
<html>
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
    <title>KineticJS Simple Game</title> <!-- Style Main -->
    <link type="text/css" rel="stylesheet" href="app/stylesheets/main.css"/> <!-- Require Main -->
    <script type="text/javascript" data-main="app/javascript/main" src="app/javascript/lib/require.js"></script>
</head>
<body> <!-- Kinetic Canvas -->
<div id="container">
    <div id="loading-info">LOADING...</div>
</div> <!-- Key detection --> <a href="javascript:void(0);" id="anchor"></a></body>
</html>


Стили

MAIN.CSS

Файл main.css инкапсулирует все импортированные стили.

@import url('defaults.css');
@import url('body.css');
@import url('typography.css');

DEFAULTS.CSS

В defaults.css происходит перезагрузка встроенных стилей для браузеров. Эдакий хак, чтобы отображать содержимое одинаково на большинстве браузеров.

/* Defaults */
* {
    padding: 0;
    margin: 0;
    border: 0;
    position: relative;
    color:#FFFFFF;
    background-color: transparent;
    letter-spacing: 0px;    
}

TYPOGRAPHY.CSS

В файле typography.css объявляем загрузку шрифтов. Шрифты можно инициализировать как через загрузчик google, так и через файловую систему, установив в директорию файл в формате *.ttf

/* @import url(http://fonts.googleapis.com/css?family=Prosto+One); */
/* ИЛИ */ 
@font-face {
    font-family: 'Prosto One';
    src: url(../fonts/Prosto_One.ttf);
}

BODY.CSS

body {
    width: 100%;
    height: 100%;
    margin: 0;
    padding: 0;
    background-color: transparent;
}

#container {
    position: absolute;
    padding: 10px;
    margin: 10px;
    width: 640px;
    height: 360px;
    box-shadow: 0px 0px 15px 0px rgba(0, 15, 60, 0.83);
}

#loading-info {
    position: relative;
    z-index: 999;
    color: black;
    text-align: center;
    font-size: 72px;
    margin: 144px auto;/* центрируем (360/2)-(72/2) */
    padding: 0;
}

Скрипты

MAIN.JS

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

require.config({
    baseUrl: './app/javascript/usr',
    removeCombined : true,
    optimize : 'none',
    paths: {
        //cdn или локальный ресурс
        jquery : [
                  '//cdnjs.cloudflare.com/ajax/libs/jquery/2.0.3/jquery.min',
                  '../lib/jquery-2.0.3.min'
                  ],
        //only local
        kinetic : '../lib/kinetic-v5.0.1.min',
        domReady : '../lib/domReady'
    }
});

require([
    'game'
    ,'controls'
    ,'lvlGame'
    ,'jquery'
    ,'kinetic'
    ,'domReady'
        ], function(game, controls, lvlGame, $) {
        "use strict";

        var $anchor = $('#anchor'),
   $container = $('#container');
  
  $anchor.focus();
  
  // привязка считывания собыий с клавиатуры или прочих устройств I/O
  $anchor.on('keydown', controls.keyDown);
  // привязка считывания клика и тапа 
  $container.on('click touchstart', controls.click);
   
        // создание canvas контейнера
        game.stage = new Kinetic.Stage({
            container: 'container',
            width: 640,
            height: 360
        });

        // создание игры
        lvlGame.initialize(function() {
            $('#loading-info').remove();
        });
    }
);

GAME.JS

Это модуль содержит главный главный игровой объект Game.

define(
    'game',
    function( ) {
        return window.Game || {};
    }
);

CONTROLS.JS

Я рекомендую отделять события от содержимого и создать модуль controls.js. С его помощью будем настраивать управление.

define(
    'controls',
    ['lvlGame'],
    function(lvlGame) {
        "use strict";

        return {
            keyDown : function(event) {
                var keyCode = event.keyCode;

                switch (keyCode) {
                    //enter, space or P-key
                    case 13 :
                    case 32 :
                    case 80 : {
                        //запуск игры
                        lvlGame.play();
                        break;
                    }

                    default: break;
                }
            },
   click : function(event) {
    lvlGame.play();
   }
        };
    }
);

LVLGAME.JS

Главный файл содержащий всю игровую логику.

define(
'lvlGame',
['game'],
function(game) {
"use strict";
game.init = null;// прототип инициализатора
var timer = null;// интервал таймера
var cardSize = { w: 110, h: 110 };
var gameObj = {};
// загрузка игровых объектов
function initGameObj() {
gameObj.layerBackground = null;
gameObj.layerGame = null;
gameObj.startTimer = 30.0 ; // время для игры
}
// перезагрузка игровых объектов
function resetGameObj() {
updateGameTimerText("Let's go!", 'white');
gameObj.layerGame.removeChildren();
gameObj.cardAnimationsName = ['type1', 'type2', 'type3', 'type4', 'type5'];
gameObj.timer = 0.0;        // текущее значение интервала
gameObj.tempTypes = [];     // временные типы карт
gameObj.tempCards = [];    // временные сохраненные карты
gameObj.cards = [];         // карты на уровне
gameObj.cardGroup = null;   // группа карт
gameObj.cardAnimations = {  // анимации карт
//      x,      y,      width,         height (1 frame)
idle:   [ 0,    0,      cardSize.w,    cardSize.h ],
type1:  [ 110,  0,      cardSize.w,    cardSize.h ],
type2:  [ 220,  0,      cardSize.w,    cardSize.h ],
type3:  [ 0,    110,    cardSize.w,    cardSize.h ],
type4:  [ 110,  110,    cardSize.w,    cardSize.h ],
type5:  [ 220,  110,    cardSize.w,    cardSize.h ]
};
gameObj.gameTime = 0.0      // время игры
}
// события мыши
function mouseOver() {
// защита от дурака
if(!gameObj.timer) return;
if(document.body.style.cursor !== 'pointer') {
document.body.style.cursor = 'pointer';
}
}
function mouseOut() {
if(document.body.style.cursor !== 'default') {
document.body.style.cursor = 'default';
}
}
// упаковочные ресурсы игры
var sources = {
background: 'resource/backgrounds/background.png',
card: 'resource/sprites/cards.png'
};
// асинхронная установка картинок из упаковочных ресурсов игры
function loadImages(sources, callback) {
var images = {},
loadedImages = 0,
numImages = 0,
src = null;
for(src in sources) {
numImages++;
}
for(src in sources) {
images[src] = new Image();
images[src].onload = function() {
if(++loadedImages >= numImages) {
callback(images);
}
};
images[src].src = sources[src];
}
}
// прототип инициализации. содержит внутриигровые объекты
function Init() {
gameObj.layerBackground = new Kinetic.Layer({
clearBeforeDraw: true
});
gameObj.layerGame = new Kinetic.Layer({
clearBeforeDraw: true
});
}
Init.prototype.background = function(image) {
var backImage = new Kinetic.Image({
image: image,
width: game.stage.getWidth(),
height: game.stage.getHeight()
});
gameObj.layerBackground.add(backImage);
return this;
};
Init.prototype.cards = function(img) {
gameObj.cardGroup = new Kinetic.Group({});
var i = gameObj.cardAnimationsName.length,
j,
tempElem = [],
paddingLeft = 10,
paddingTop = 10,
left = 20,
top = 20,
tempAnimName = gameObj.cardAnimationsName;
while(i--) {
// выбор случайных карт
var randElem = tempAnimName.splice(Math.floor(Math.random() * (i + 1)), 1)[0];
// сразу вставляем по две карты
tempElem.push(randElem);
tempElem.push(randElem);
}
// псевдо-случайная сортировка карт по уровню
tempElem.sort(function() {
return parseInt( Math.random() * 10 ) % 2;
});
// располагаем карты в два ряда по пять карт
for(i = 0; i < 2; i++) {
for(j = 0; j < 5; j++) {
// очистка первой временной карты
var cardType = tempElem.pop();
if(cardType) {
gameObj.cards.push(
new card(
cardType,
img,
left + j * (cardSize.w + paddingLeft),
top + i * (cardSize.h + paddingTop)
)
);
}
}
}
return this;
};
Init.prototype.text = function(text) {
// создание текстового элемента
var textTimer = new Kinetic.Text({
x: 0,
y: game.stage.getHeight() - 64, //специально для двух строк (30*2 и расстояние)
width: game.stage.getWidth() - 10,
text: text || '',
fontFamily: 'Prosto One',
fontSize: 30,
fill: 'white',
shadowColor: "black",
shadowOpacity: .8,
shadowBlur: 8,
align: 'right',
listening: false,
id: 'textTimer'
});
// установка текста на игровой слой
gameObj.layerBackground.add(textTimer);
return this;
};
// обновление текста таймера
function updateGameTimerText(text, color) {
         // получение элемента по id элемента
var timeElem = game.stage.get('#textTimer')[0];
if(text) {
timeElem.setText(text);
}
if(color) {
timeElem.setFill(color);
}
// быстрая перерисовка сцены
timeElem.getLayer().batchDraw();
}
// таймер игры
function gameTimer() {
var timerValue = --gameObj.timer;
updateGameTimerText("TIME:tt" + timerValue + "'s");
gameObj.gameTime = gameObj.startTimer - timerValue;
// когда таймер завершается - гамовер
if(gameObj.timer === 0) {
gameOver();
}
}
// название карты, картинка, координаты
var card = function(type, img, x, y) {
var _this = this; //ссылка на текущий объект
this.type = type; //уникальный тип карты
this.show = false; //флаг что карта открыта
// спрайт карты
var cardSprite = new Kinetic.Sprite({
x: x || 0,
y: y || 0,
image: img,
animations: gameObj.cardAnimations,
animation: 'idle',
index: 0,
frameRate: 0
});
// запил внутрь спрайта прямоугольник
// TODO это плохая практика. Убрать отсюда
cardSprite.rectBackground = new Kinetic.Rect({
x: x,
y: y,
width: cardSize.w + 2,
height:cardSize.h + 2,
fill: 'skyblue',
//                есть глюк движка, когда внутри одного спрайта рисовать другой - начинает ползти тень
shadowColor: 'black',
shadowBlur: '12',
shadowOpacity:1,
//                kinetic bug: Можно использовать либо stroke, либо shadow
//                stroke: 'black',
//                strokeWidth: 1,
opacity: 1,
listening: false
});
// событие клика внутри по текущей карте
function cardClick() {
// защита от дурака
if(!gameObj.timer || 
cardSprite.getAnimation() !== 'idle') {
return;
}
// количество открытых карт на данный момент
var pushedCount = gameObj.cardGroup.children.filter(function(elem) {
return elem.getAttr('pushed')
}).length;
// контейнеры для карт (используются как сохранение состояний уже выбранных карт)
if(pushedCount < 2) {
gameObj.tempCards.push(cardSprite);
gameObj.tempTypes.push(_this.type);
cardSprite.setAnimation(_this.type);
cardSprite.rectBackground.setFill('white');
cardSprite.setAttr('pushed', true);
}
// если выбраны две карты
if(gameObj.tempTypes.length === 2) {
var isSame = gameObj.tempTypes.every(function(value) {
return value === _this.type;
});
// если две карты одинаковые
if(isSame) {
//ставим флаг что карты открыты
gameObj.tempCards.forEach(function(value){
value.showed = true;
value.setAttr('pushed', false);
});
gameObj.cards.length -= 2;
// если карт не осталось - уровень пройден
if(gameObj.cards.length === 0) {
gameWin();
}
} else {
// иначе поворачиваем открытые карты обратно
gameObj.tempCards
.filter(function(value) {
return !value.showed;
})
.forEach(function(value) {
var _value = value;
setTimeout(function() {
_value.setAnimation('idle');
_value.rectBackground.setFill('skyblue');
_value.setAttr('pushed', false);
_value.getLayer().batchDraw();
}, 800);    
});
}
// очистка контейнеров объектов карт и их типов
gameObj.tempCards.length = 0;
gameObj.tempTypes.length = 0;
}
// перерисовка состояния всего уровня
game.stage.clear()
game.stage.draw();
}
// привязка событий
cardSprite.on('mouseover', mouseOver);
cardSprite.on('mouseout', mouseOut);
cardSprite.on('click touchend', cardClick);
// формирование группы из карт
gameObj.cardGroup.add(cardSprite);
gameObj.layerGame.add(cardSprite.rectBackground)
// установка карты на игровой слой
gameObj.layerGame.add(cardSprite);
// z-index надо вызывать после объекта установки на слой
cardSprite.rectBackground.setZIndex(0);
cardSprite.setZIndex(1);
};
// проигрыш
function gameOver() {
clearInterval(timer);
updateGameTimerText(
"Game Over!" +
"nPress enter key to restart game",
'red'
);
removeCards();
}
// выигрыш
function gameWin() {
clearInterval(timer);
updateGameTimerText(
"CONGRATULATIONS!" +
"nYour time:t" + gameObj.gameTime + "'s",
'gold'
);
}
// удаление всех карт с уровня
function removeCards() {
if(gameObj.cardGroup.hasChildren()) {
// удаление масок карт
gameObj.cardGroup.children.filter(function(elem) {
return elem.rectBackground
}).forEach(function(elem) {
elem.rectBackground.destroy();
});
// удаление карт
gameObj.cardGroup.children.destroy();
}
}
// обертка для вызова через require.js
return {
// функция инициализации игрового мира
initialize : function(callback) {
initGameObj();
// загрузка ресурсов
// инициализация полустатических объектов
loadImages(sources, function(sources) {
game.init = new Init()
.background(sources.background)
.text("Press Enter or Click" +
"n to start the game"
);
// формирование заднего слоя
game.stage.add(gameObj.layerBackground);
callback();
});
},
play: function() {
if(!gameObj.timer) {
//очистка данных
resetGameObj();
clearInterval(timer);
// загрузка ресурсов и последующая за ним функция инициализаций уровня
loadImages(sources, function(sources) {
// инициализация игровых объектов
game.init.cards(sources.card);
// формирование игрового слоя
game.stage.add(gameObj.layerGame);
// обновляем значение интервала
gameObj.timer = gameObj.startTimer;
// перезапуск таймера
timer = setInterval(gameTimer, 1000);
});
}
}
};
}
);

Итог второй части

KineticJS показала нам насколько это гибкая технология для создания браузерных игр. Используя её вместе с библиотекой require.js можно строить мощные абстрактные слои для облегчения читаемости кода. KineticJS полностью совместим для разработки мобильных игр. В третьей части я расскажу про инструмент для портирования HTML5 игр на мобильные платформы (Android, iOS, WinPhone).

В третьей части постараемся оформить полученную игру в виде Android приложения.

Денис Сергеевич Басковский

Философ, изобретатель и поэт.

Подписаться
Уведомить о
guest
0 комментариев
Межтекстовые Отзывы
Посмотреть все комментарии
html5 game
Предыдущая статья

Создаем HTML5 игру. Часть 1: Выбор технологий

Следующая статья

Создаем HTML5 игру. Часть 3: Портируем на Android