На этот раз я расскажу вам как создавать браузерные HTML5 игры с помощью игрового движка Impact. Я долго думал какого жанра должна быть наша игра и решил, что это будет простенькая бродилка, на основе которой вы вполне сможете создать собственную мега-игру.
Щелкнув по этой ссылке, вы сможете скачать исходные коды нашей HTML5 браузерной игры, которые используются в данной статье.
Структура по умолчанию
После разархивирования файла Impact вы получите пустой набор данных со структурой, готовой к работе.
- media/ -> Ассеты игры (изображения и звуки)
- lib/ -> Игра и движок
- lib/game/
- lib/game/entities/
- lib/game/levels/
- lib/impact/ -> Движок
- lib/weltmeister/ -> Редактор уровней
Желательно использовать данную инструкцию, но ничто вам не мешает выполнять все действие на ваше усмотрение. Чтобы начать игру, откройте файл index.html
в вашем браузере.
Модули
Как использовать системные модули :
ig.module(
'game.my-file'
)
.requires(
'impact.game',
'impact.image',
'game.other-file'
)
.defines(function(){
// Код модуля
});
Определение модуля происходит в 3 этапа:
- Название модуля в ig.module()
- Зависимости в .requires()
- Код в модуля в .defines()
Классы
Impact использует классический способ наследования. Метод .extend() используется для создания нового класса от родительского класса:
// Создание класса на основе исходного класса (ig.Class)
var Person = ig.Class.extend({});
// Создание класса Ninja из класса Person
var Ninja = Person.extend({});
Метод .init() автоматически вызывается конструктором класса, то есть каждый раз при создании экземпляра:
var InitTest = ig.Class.extend({
init: function(param) {
console.log("Init вызывается "+param);
}
});
new InitTest("ZOMG"); // => Init вызывается с ZOMG
Метод .parent() позволяет вызывать текущий метод родительского класса: - как бы это порусски?
// Создание класса "Person"
var Person = ig.Class.extend({
name: "",
init: function(name) {
this.name = name; // Присваиваем имя, которое будет
// передаваться в качестве параметра
}
});
// Создание класса "Ninja", который наследует от класса "Person"
var Ninja = Person.extend({
init: function(name) {
// Вызываем исходную функцию
this.parent("Ниндзя: "+name);
}
});
// Создание объекта "Person"
var person = new Person("Generic Person");
person.name; // => Generic Person
// Создание объекта "Ninja"
var ninja = new Ninja("Вася Пупкин");
ninja.name; // => Ниндзя: Вася Пупкин
Редактор уровней. Создание уровня
А теперь, приступим к самому интересному: созданию первого уровня, используя редактор уровней Weltmeister. Для работы этого редактора уровней, вы должны запустить его на сервере Apache с PHP.
Альтернативные способы запуска на ruby, nodejs, python и др. доступны в ознакомительной части
Скопируйте файлы на сервер и откройте страницу редактора уровня: http://yoursite.ru/impact/weltmeister.html
. Первоначально у вас есть пустой уровень untitled.js.
Мы создадим новый слой для рисования поверхности нашего уровня. Нажмите +
в непосредственной близости от метки Слои (Layers) в панели справа:
Появится список настроек. Введите имя : main.
Теперь займемся настройкой поверхности, для чего нам нужно перейти к опции tileset.
Вы должны указать размер плитки (Tilesize) в пикселях и количество плитки на этом слое:
Нажмите на кнопку Apply Changes (Принять изменения):
Теперь давайте нарисуем наш игровой мир. Нажмите на map (карта), которая на данный момент пуста, а затем на клавишу "пробел" чтобы выбрать плитку:
Вы можете разместить свои плитки для создания уровня:
Вы можете увеличить или уменьшить масштаб с помощью колеса мыши и перемещать изображение удерживая нажатой правую кнопку мыши.
Если вы хотите удалить плитку, нажмите клавишу пробел и выберите пустой оранжевый квадрат, который работает как ластик.
Вы можете выбрать несколько плиток сразу, удерживая нажатой клавишу SHIFT
Мы добавим столкновений, для чего создайте новый слой с помощью клавиши +
, а затем выберите параметр Is Collision Layer. Измените размер квадрата столкновения, который должен быть идентичен другому слою:
Нажмите на карту, и нажмите пробел, чтобы выбрать тип столкновения. Выберите статичные столкновений и разместить их в нужных местах:
Когда уровень пройден и вам необходимо его сохранить, нажмите на save, перейдите в папку lib/game/levels
, присвойте уровню имя (level1.js
, например) и нажмите save.
Не забудьте предоставить права на запись папке levels/.
Загрузка уровня
Скачайте файлы с компонентами для нашей главы и принимайтесь за работу.
Вернемся к каталогу основной игры, извлеченного из Impact, и проанализируем его перед изменениями, необходимыми для загрузки только что созданного нами уровня:
ig.module(
"game.main" // Имя модуля main
)
// Включение зависимостей
.requires(
"impact.game",
"impact.font"
)
.defines(function(){
// Создание игры
MyGame = ig.Game.extend({
// Загрузка шрифта
font: new ig.Font("media/04b03.font.png" ),
init: function() {
// Инициализация игры
},
update: function() {
// вызываем игровой цикл (gameloop),
// который обновляет объекты и карту
this.parent();
// Здесь мы можем добавить нашe собственный код
},
draw: function() {
// Здесь мы вызываем функцию, которая рисует сущности
// и карту на холсте
this.parent();
// Здесь мы можем добавить наш собственный код
var x = ig.system.width/2,
y = ig.system.height/2;
this.font.draw( "Смотрите, работает!", x, y, ig.Font.ALIGN.CENTER );
}
});
// Игра начинается с параметрами: 60fps, разрешение 320x240, масштабирование x2
ig.main( "#canvas", MyGame, 60, 320, 240, 2 );
});
Чтобы добавить в свою игру уроень, его необходимо загрузить с помощью .requires():
.requires(
'impact.game',
'game.levels.level1' // game/levels/level1.js
)
Далее в .init(), а следовательно, при начальном запуске игры, делаем запрос на загрузку уровня:
init: function() {
// Уровень загрузки
this.loadLevel(LevelLevel1);
}
Проверим работу скрипта, для этого наберите в браузере http://localhost/impact/
.
Создание персонажа
Добавим гравитацию в нашу игру.
В файл main.js
добавим свойство .gravity
.
gravity: 275
Затем добавить управление с помощью клавиатуры в main.js
с помощью ig.input.bind().
init: function() {
// Определение управления
ig.input.bind(ig.KEY.LEFT_ARROW, 'left');
ig.input.bind(ig.KEY.RIGHT_ARROW, 'right');
ig.input.bind(ig.KEY.X, 'jump');
ig.input.bind(ig.KEY.C, 'shoot');
// Загрузка уровня
this.loadLevel(LevelLevel1);
}
Создать новый файл game/entities/player.js
в котором мы будем строить класс PlayerEntity
, который поможет нам создать нашего игрового персонажа.
Для начала присвоим модудю имя : ig.module('game.entities.player')
и зависимости: .requires('impact.entity')
. Тогда .defines() мы можем создать класс EntityPlayer на основе класса Entity: EntityPlayer = ig.Entity.extend()
.
ig.module(
'game.entities.player'
).requires(
'impact.entity'
).defines(function() {
EntityPlayer = ig.Entity.extend({
Теперь нужно задать свойства нашего персонаже:
// Спрайт с персонажем
animSheet: new ig.AnimationSheet('media/player.png', 17, 17),
// Размер элемента (размер спрайта – размер краев)
size: { x: 11, y: 17 },
// Внутренний край
offset: { x: 3, y: 0 },
// Спрайт не повернут
flip: false,
// Максимальная скорость
maxVel: { x: 100, y: 150 },
// Трение с землей
friction: { x: 600, y: 0 },
// Импульс прыжка
jump: 225,
Затем мы создадим функцию инициализации сущности, которая вызывается каждый раз, когда создается новый объект. Вы должны вызвать функцию .parent()
, которая будет вызывать функцию инициализации .init()
родительского класса (ig.Entity) для инициализации и позиционирования персонажа. После вы должны определить необходимые анимации с помощью функции .addAnim():
- Первый параметр этой функции - имя анимации
- Второй - задержка перед сменой кадра (в секундах)
- Третий - список проигрываемых в заданном порядке кадров
init: function(x, y, settings) {
this.parent(x, y, settings);
// Анимации
this.addAnim('idle', 1, [0]);
this.addAnim('run', 0.07, [0, 1, 2, 3, 4, 5, 6, 7, 8]);
this.addAnim('jump', 1, [7]);
this.addAnim('fall', 0.1, [7, 8]);
},
Теперь мы создадим игровой цикл: .update(). Этот метод вызывается 60 раз в секунду.
Мы будем проверять нажал игрок клавишу со стрелкой влево или вправо с помощью ig.input.
Если игрок нажал клавишу со стрелкой влево, мы применяем отрицательное ускорение. Если со стрелкой вправо - то положительное.
A la fin de la boucle .update() la méthode .parent() est appelée, celle-ci traitera automatiquement cette accélération pour déplacer le personnage.
Если персонаж стоит на земле (this.standing) и нажата клавиша прыжка, то мы изменяем скорость по вертикали (this.vel.y
).
Если игрок нажмет кнопку движения влево, мы должны будем отразить по-горизонтали соответствующий кадр нашего спрайта (т.к. он правоориентированный)
Ensuite il faut sélectionner l’animation appropriée (this.currentAnimation) :
- Если скорость по-вертикали отрицательная - применяем анимацию jump (прыжок).
- Если скорость по-вертикали отрицательная - применяем анимацию fall (падение).
- Если ускрорение отрицательное - применяем анимацию run (движение).
- Если игрок не бежит, не прыгает и не падает - применяем анимацию idle (бег на месте).
update: function() {
var accel = 350;
// Перемещение
if (ig.input.state('left')) {
this.accel.x = -accel;
this.flip = true;
} else if (ig.input.state('right')) {
this.accel.x = accel;
this.flip = false;
} else {
this.accel.x = 0;
}
// Прыжок
if (this.standing && ig.input.pressed('jump')) {
this.vel.y = -this.jump;
}
// Выбор анимации движения
if (this.vel.y < 0) {
this.currentAnim = this.anims.jump;
} else if (this.vel.y > 0) {
this.currentAnim = this.anims.fall;
} else if (this.vel.x != 0) {
this.currentAnim = this.anims.run;
} else {
this.currentAnim = this.anims.idle;
}
// Направление спрайта
this.currentAnim.flip.x = this.flip;
this.parent();
}
});
});
Теперь мы хотим добавить нашего персонажа на ранее созданный уровень.
Откройте файл level1.js
в редакторе уровней, нажмите entities
(сущности), а затем нажмите пробел также, как для плитки.
Выберите Player
(плеер) в раскрывающемся меню:
Затем поместите игрока, где вам хочется:
Создание врага
Точно также как и класс EntityPlayer, мы создаем файл games/entities/zombie.js
, а в нем класс EntityZombie.
ig.module(
'game.entities.zombie'
).requires(
'impact.entity'
).defines(function() {
EntityZombie = ig.Entity.extend({
animSheet: new ig.AnimationSheet('media/zombies.png',17,17),
size: { x: 11, y: 17 },
offset: { x: 3, y: 0 },
maxVel: { x: 100, y: 100 },
flip: false,
friction: { x: 300, y: 0 },
speed: 24, // Скорость движения
init: function(x, y, settings) {
this.parent(x, y, settings);
// Анимация
this.addAnim('walk', .07, [0, 1, 2, 3, 4, 5]);
},
В цикле .update() мы перемещаем зомби в направлении, определенном в this.flip.
Цикл .handleMovementTrace() обрабатывает столкновения на карте.
Когда зомби сталкиваются со стеной, мы меняем направление движения.
update: function() {
// Определяем скорость и направление зомби
var direction = this.flip ? -1 : 1;
this.vel.x = this.speed * direction;
this.currentAnim.flip.x = this.flip;
this.parent();
},
handleMovementTrace: function(res) {
this.parent(res);
// Разверните зомби, когда он натолкнется на стену
if (res.collision.x) {
this.flip = !this.flip;
}
}
});
});
Добавьте еще несколько зомби на наш уровень.
Улучшения
Сначала я буду говорить о системе столкновений Impact.
Есть столкновения статические
(Entity (сущность) и World (мир)), они управляются методом .handleMovementTrace(), как уже было описано выше.
И есть столкновения динамические
(Entity vs Entity), они управляются методом .check().
Динамические столкновения бывают нескольких типов. TYPE позволяет разбивать сущности по группам, чтобы потом легко управлять столкновениями одной группы с другой. Существует три типа столкновений:
Свойство .checkAgainst определяет, против какого типа TYPE объект должен управлять столкновениями. Есть 4 возможных типа:
- TYPE.NONE
- TYPE.A
- TYPE.B
- TYPE.BOTH (TYPE.A et TYPE.B)
И несколько режимов:
- NEVER
- Все столкновения игнорируются для этого объекта. Например: декоративные элементы.
- LITE
- Если два объекта LITE сталкиваются, они могут стать преградой друг другу. Например: частицы.
- PASSIVE
- Если два объекта PASSIVE сталкиваются, они могут стать преградой друг другу. Например: игрок.
- ACTIVE
- Если один объект ACTIVE сталкивается с более слабым объектом (LITE или PASSIVE), он будет блокировать последнего. Например: ящики.
- FIXED
- Объект не будет двигаться вследствие collision (столкновения). Например: платформы, которые перемещаются.
Я предлагаю вам прочитать этот учебник по столкновениям, чтобы разобраться в этом вопросе.
Давайте начнем с самого простого части: смерти игрока при столкновении с зомби. Вернемся к файлу player.js
и добавим следующие свойства столкновения:
type: ig.Entity.TYPE.A,
checkAgainst: ig.Entity.TYPE.NONE,
collides: ig.Entity.COLLIDES.PASSIVE,
Затем откройте zombie.js
и добавьте следующие свойства столкновения:
type: ig.Entity.TYPE.B,
checkAgainst: ig.Entity.TYPE.A,
collides: ig.Entity.COLLIDES.PASSIVE,
Далее перегружаем метод .check() , чтобы убить игрока, когда он натыкается на зомби:
check: function(other) {
other.kill();
}
Теперь, когда зомби может убить игрока, мы должны дать ему способ защитить себя. Пусть это будут гранаты. Создадим games/entities/grenade.js :
Не забудьте назначить .checkAgainst
TYPE.B (тип зомби) и добавить новое свойство .bounciness, которое характерезует вероятность отказов гранаты.
ig.module(
'game.entities.grenade'
).requires(
'impact.entity'
).defines(function() {
EntityGrenade = ig.Entity.extend({
size: { x: 4, y: 4 },
offset: { x: 2, y: 2 },
animSheet: new ig.AnimationSheet('media/grenade.png', 8, 8),
type: ig.Entity.TYPE.NONE,
checkAgainst: ig.Entity.TYPE.B,
collides: ig.Entity.COLLIDES.PASSIVE,
maxVel: { x: 200, y: 200 },
bounciness: 0.6, // Показатель отказов
bounceCounter: 0, // Nombre de rebonds effectués
В .init() мы изначально позиционируем гранату в зависимости от направления и положения игрока:
- Если направление игрока находится справа, мы позиционируем гранату правее позиции игрока на 7 пикселей.
- Если направление игрока находится слева мы позиционируем гранату левее игрока на 4 пикселя.
Зададим начальную скорость гранате:
- Вертикальная скорость: -65px (т.е. вверх).
- Если направление игрока находится справа, необходимо задать положительную горизонтальную скорость и наоборот: слева - отрицательную скорость.
init: function(x, y, settings) {
x += settings.flip ? -4 : 7
this.parent(x, y, settings);
// импульс
this.vel.x = settings.flip ? -this.maxVel.x : this.maxVel.x;
this.vel.y = -65;
this.addAnim('idle', 0.2, [0, 1]);
},
В .handleMovementTrace() мы будем задавать настройки для управления временем жизни гранаты: она должна взорваться самое позднее после 3 удара обо что угодно.
handleMovementTrace: function(res) {
this.parent(res);
// Если граната сталкивается со стеной,
if (res.collision.x || res.collision.y) {
// она может подпрыгнуть
this.bounceCounter++;
// не более 3 раз
if (this.bounceCounter > 3) {
// перед взрывом
this.kill();
}
}
},
Если граната попадает в игрока, она взрывается и убивает его.
check: function(other) {
other.kill();
this.kill();
}
});
});
Теперь необходимо включить гранату в число зависимостей модуля плеера.
.requires(
'impact.entity',
'game.entities.grenade' // EntityGrenade
)
В цикле .update() мы показываем гранату с помощью метода .spawnEntity(), когда игрок нажимает на кнопку стрельбы.
.spawnEntity() принимает четыре параметра:
- объект, который нужно создать
- координату по оси x;
- координату по оси y;
- необязательно - объект с дополнительными данными;
update: function() {
// ...
// Граната
if(ig.input.pressed('shoot')) {
ig.game.spawnEntity(EntityGrenade, this.pos.x, this.pos.y, {
flip: this.flip
});
}
// ...
}
Умереть и ожить
В нашей текущей игре имеется один косяк - если игрок умирает, игра все равно продолжается. В этой статье мы сделаем минимум: осуществим перезапуск текущего уровня при гибели игрока.
Surchargeons la méthode .kill() de PlayerEntity, cette méthode est appelée à la mort du joueur.
Мы будем использовать .loadLevelDeferred() вместо .loadLevel().
ерегружаем метод .kill()
из PlayerEntity
, этот метод вызывается в случае смерти игрока.
kill: function() {
// Перезагрузка уровеня когда игрок умирает
ig.game.loadLevelDeferred(ig.global.LevelLevel1);
}
Частицы и взрывы
Создадим генератор частиц для изображения взрывов и брызг крови.
Картинка particles.png состоит из 2 частиц размером 2x2px; одна для взрыва, вторая для крови.
Создадим файл game/entities/particles.js
:
ig.module(
'game.entities.particle'
).requires(
'impact.entity'
).defines(function() {
EntityParticle = ig.Entity.extend({
size: { x: 2, y: 2 },
maxVel: { x: 160, y: 200 },
lifetime: 1, // время жизнь: 1 секунда
bounciness: 0,
vel: { x: 100, y: 100 }, // средняя скорость
friction: { x:100, y: 0 },
collides: ig.Entity.COLLIDES.LITE,
animSheet: new ig.AnimationSheet('media/particles.png',2,2),
В .init() мы:
- Устанавливаем тип отображаемых частиц (кровь или взрыв).
- Генерируем случайную скорость.
- Устанавливаем таймер для жизни частицы.
init: function(x, y, settings) {
this.parent(x, y, settings);
// Выбор частицы в соответствии с типом
var types = {
blood: 0,
explosion: 1
};
this.addAnim('idle', 0.2, [types[settings.type]]);
// произвольная скорость для изображения взрыва
this.vel.x = (Math.random()*2-1) * this.vel.x;
this.vel.y = (Math.random()*2-1) * this.vel.y;
// Создание таймера жизни частицы
this.lifeTimer = new ig.Timer();
},
В цикле .update()
мы будем уничтожать частицу в конце ее жизненного цикла и уменьшать ее видимость с течением времени с помощью свойства .alpha.
update: function() {
// Когда в таймере наступит момент конца жизни
// частицы, она уничтожится
if(this.lifeTimer.delta() > this.lifetime) {
this.kill();
return;
}
// рассчет параметров альфа-канала в зависимости от
// к-ва времени, требуемого для эффекта затемнения частицы
this.currentAnim.alpha = this.lifeTimer.delta().map(
0, this.lifetime,
1, 0
);
this.parent();
}
});
Теперь, когда наша система частиц будет готова, она должна быть добавлена к зависимостям нашей игры
EntityParticlesEmitter = ig.Entity.extend({
particles: 25, // Число сгенерируемых частиц
init: function(x, y, settings) {
this.parent(x, y, settings);
// Генерация частиц
for(var i = 0; i < this.particles; i++) {
ig.game.spawnEntity(EntityParticle, x, y, {
type: settings.type
});
}
// Немедленное удаление генератора
this.kill();
}
});
});
Сейчас, когда наша система частиц готова, нужно добавить ее к зависимостям нашей игры.
.requires(
'impact.game',
'game.levels.level1', // game/levels/level1.js
'game.entities.particle' // game/entities/particle.js
)
Когда граната взрывается должны генерироваться частицы взрыва (grenade.js)
kill: function() {
// Генерация взрыва
ig.game.spawnEntity(EntityParticlesEmitter, this.pos.x, this.pos.y, {
type: "explosion"
});
this.parent();
}
Когда зомби умирает должны генерироватся частицы крови (zombie.js).
kill: function() {
// Генерация брызг крови
ig.game.spawnEntity(EntityParticlesEmitter, this.pos.x, this.pos.y, {
type: "blood"
});
this.parent();
}
Камера
Мы немного изменим данные нашего уровня, чтобы он выступал за рамки экрана по ширине (например, на 50 или 100 плит).
Потом мы отредактируем нашу камеру. Это нужно сделать в цикле .update() из main.js
:
С помощью метода .getEntitiesByType() мы восстановим сущности нашего игрока, а, значит, и его место нахождение.
Также мы будем блокировать камеру когда игрок находится на краю карты.
update: function() {
// Восстановление сущности нашего игрока
var player = this.getEntitiesByType(EntityPlayer)[0];
// Если объект существует, мы сосредотачиваем камеру на персонаже
if(player) {
// Определяем ширину уровня в пикселях
var levelWidth = ig.game.backgroundMaps[0].width*ig.game.backgroundMaps[0].tilesize;
// Центрируем экран относительно персонажа
var centeredPosition = player.pos.x-ig.system.width/2;
// Камера останавливается на краю уровня
this.screen.x = Math.min(
Math.max(centeredPosition, 0),
levelWidth-ig.system.width
);
}
this.parent();
}
Пользовательский интерфейс
Последний штрих, который остался - этодобавить инструкциям на экране. Для этого мы будем использовать библиотеку font, входящую в Impact. Мы должны добавить ее к нашим зависимостям игры (main.js).
.requires(
'impact.game',
'impact.font',
'game.levels.level1',
'game.entities.particle'
)
Добавить font
в класс MyGame.
instructions: new ig.Font('media/04b03.font.png'),
Теперь мы можем его использовать; мы перегружаем цикл .draw()
игры, чтобы вывести на экран инструкции с помощью метода .draw() из font
.
draw: function() {
this.parent();
var controls = '[Controls] &larr,→: перемещение, X: прыжок & C: срельба';
var zombiesLeft = 'осталось зомби: '+this.getEntitiesByType(EntityZombie).length;
// Инструкции расположены внизу по-центру экрана
var x = ig.system.width/2;
var y = ig.system.height-8;
// Рисуем инструкции
this.instructions.draw(controls, x, y, ig.Font.ALIGN.CENTER);
var y = 5;
// Количество оставшихся зомби
this.instructions.draw(zombiesLeft, x, y, ig.Font.ALIGN.CENTER);
}
Отладка
Impact имеет инструмент для отладки и мониторинга. Чтобы активировать его, достаточно добавить impact.debug.debug
в зависимости игры и бар отладки автоматически отобразится в нижней части экрана.
.requires(
'impact.debug.debug'
)
Оптимизация
Если вы хотите опубликовать свою игру в сети, вы можете ее минимизировать и поместить в 1 файл с помощью простого PHP скрипта, который находится в архиве Impact.
Для Windows сценарий выполняется с tools/bake.bat
.
Для MacOSX сценарий выполняется с tools/bake.sh
.
Или вы можете запустить ее прямо с командной строки: $ php tools/bake.php lib/impact/impact.js lib/game/main.js my_game.min.js
Наш урок подошел к концу. Удачи!