О чем этот пример
При разработке игр часто требуется использовать кнопки с одинаковым поведением в разных сценах. Создание отдельного класса для кнопки не только делает код чище, но и позволяет централизовано управлять её логикой и внешним видом. В этой статье мы разберем, как создать собственный компонент кнопки с анимацией нажатия и текстом, который легко интегрируется в любую сцену вашего проекта на Phaser 3.
Версия Phaser: код и демо в этой статье рассчитаны на Phaser 3.90.0.
Живой запуск
Ниже встроен рабочий билд примера. Оригинальный источник: GitHub.
Исходный код
class ButtonComponent extends Phaser.GameObjects.Container {
constructor(config) {
super(config.scene);
this.config = config;
this.spawnButton();
}
spawnButton(){
this.x = this.config.x;
this.y = this.config.y;
this.background = this.scene.add.image(0,0,this.config.background);
this.background.setInteractive();
this.background.on('pointerdown',this.onPush, this);
this.background.on('pointerup', this.onPull, this);
this.background.on('pointerout', this.onOut, this);
this.text = this.scene.add.text(0,0,'test',{
fontSize: 120 * this.scene.game.scaleHeight * 3,
fontFamily: 'Tahoma',
padding: 10,
lineSpacing: 20,
align: 'center',
fill: '#ffffff',
wordWrap: {
width: this.background.displayWidth - 10,
}
});
this.firstScale = this.background.scale;
this.add(this.background);
this.add(this.text);
this.scene.add.existing(this);
}
destroy(fromScene){
super.destroy(fromScene);
}
onPush(){
this.tweenObject('push');
}
onPull() {
if (typeof this.config.onPush === "function") {
this.config.onPush();
//this.scene.scene.start('Game');
}
this.tweenObject('pull');
}
onOut() {
this.tweenObject('pull');
}
tweenObject(status) {
const pressure = (status === "push" ? 0.9 : 1);
if (typeof this.text !== "undefined") {
this.config.scene.tweens.add({
targets: this.text,
scale: this.firstScale * pressure,
ease: 'Linear',
duration: 100,
});
}
this.scene.tweens.add({
targets: this.background,
scale: this.firstScale * pressure,
ease: 'Linear',
duration: 100,
});
}
}
class Boot extends Phaser.Scene
{
constructor ()
{
super('Boot');
}
create ()
{
console.log('Boot.create');
this.scene.start('Preloader');
}
}
class Preloader extends Phaser.Scene
{
constructor ()
{
super('Preloader');
}
preload ()
{
this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
this.load.image('bg', 'assets/skies/gradient21.png');
this.load.image('logo', 'assets/sprites/phaser3-logo.png');
this.load.image('button', 'assets/sprites/button-bg.png');
this.load.image('blade', 'assets/sprites/blade.png');
}
create ()
{
console.log('Preloader.create');
this.scene.start('Title');
}
}
class Title extends Phaser.Scene
{
constructor ()
{
super('Title');
}
create ()
{
console.log('Title.create');
this.add.image(400, 300, 'bg');
const logo = this.add.image(400, -200, 'logo');
this.tweens.add({
targets: logo,
y: 200,
ease: 'Bounce.out'
});
// const button = this.add.image(400, 400, 'button');
// button.setInteractive();
// button.once('pointerdown', () => {
// this.scene.start('Game');
// });
const button = new ButtonComponent({
scene: this,
x:400, y:400,
background: 'button',
onPush:this.goToGameScene.bind(this)
});
}
goToGameScene() {
this.scene.start('Game');
}
}
class Game extends Phaser.Scene
{
constructor ()
{
super('Game');
}
create ()
{
console.log('Game.create');
this.add.image(400, 300, 'bg');
const blade = this.add.image(400, 300, 'blade');
this.tweens.add({
targets: blade,
angle: 360,
repeat: -1,
ease: 'linear'
});
blade.setInteractive();
blade.once('pointerdown', () => {
this.scene.start('Title');
});
}
}
const config = {
type: Phaser.AUTO,
parent: 'phaser-example',
width: 800,
height: 600,
backgroundColor: '#2d2d6d',
scene: [ Boot, Preloader, Title, Game ]
};
const game = new Phaser.Game(config);
Анатомия компонента: наследование от Container
Ключевая идея — создать новый класс, который наследуется от Phaser.GameObjects.Container. Контейнер позволяет группировать несколько игровых объектов (например, фон и текст) и управлять ими как единым целым.
Конструктор класса принимает объект config, который содержит все необходимые настройки: ссылку на сцену, координаты, ключ текстуры фона и колбэк-функцию.
class ButtonComponent extends Phaser.GameObjects.Container {
constructor(config) {
super(config.scene);
this.config = config;
this.spawnButton();
}
Сборка кнопки: фон, текст и интерактивность
Метод `spawnButton()` отвечает за создание визуальной части и логики взаимодействия. Он выполняет несколько важных шагов:
1. Создает фоновое изображение (`this.background`) и делает его интерактивным с помощью `setInteractive()`.
2. Назначает обработчики событий мыши (`pointerdown`, `pointerup`, `pointerout`) для реализации визуальной обратной связи.
3. Создает текстовый объект (`this.text`) с гибкими настройками стиля и переноса строк.
4. Добавляет оба объекта в контейнер и регистрирует сам контейнер в сцене через `this.scene.add.existing(this)`.
this.background = this.scene.add.image(0,0,this.config.background);
this.background.setInteractive();
this.background.on('pointerdown',this.onPush, this);
// ... другие обработчики
this.text = this.scene.add.text(0,0,'test',{ /* стили */ });
this.add(this.background);
this.add(this.text);
this.scene.add.existing(this);
Анимация нажатия с помощью Tweens
Визуальный отклик на действия игрока реализован через систему твинов Phaser (this.scene.tweens). Метод tweenObject(status) в зависимости от статуса ('push' или 'pull') меняет масштаб фона и текста.
При нажатии (pointerdown) масштаб умножается на 0.9, создавая эффект "вдавливания". При отпускании кнопки или уходе курсора (pointerup, pointerout) масштаб возвращается к исходному значению (this.firstScale).
tweenObject(status) {
const pressure = (status === "push" ? 0.9 : 1);
this.scene.tweens.add({
targets: this.background,
scale: this.firstScale * pressure,
ease: 'Linear',
duration: 100,
});
}
Интеграция в игровые сцены
Использовать компонент в сцене очень просто. Нужно создать его экземпляр, передав в конфигурации ссылку на текущую сцену (this), координаты, ключ текстуры и функцию-обработчик onPush, которая выполнится при успешном клике.
В примере кнопка в сцене Title при нажатии запускает метод goToGameScene, который переключает на сцену Game.
// В сцене Title.create()
const button = new ButtonComponent({
scene: this,
x:400, y:400,
background: 'button',
onPush:this.goToGameScene.bind(this)
});
Важные детали реализации
Обратите внимание на несколько технических моментов, которые делают компонент надежным:
* **Контекст `this`:** При назначении обработчиков событий (`on('pointerdown', this.onPush, this)`) третий аргумент гарантирует, что внутри метода `onPush` контекст `this` будет ссылаться на экземпляр `ButtonComponent`, а не на фоновое изображение.
* **Проверка на существование:** В методе `tweenObject` перед анимацией текста выполняется проверка `if (typeof this.text !== "undefined")`. Это защищает от ошибок, если кнопка создается без текстовой метки.
* **Уничтожение:** Метод `destroy(fromScene)` вызывает родительский метод, что обеспечивает корректное удаление всех дочерних объектов и очистку памяти.
Что попробовать дальше
Создание переиспользуемых компонентов, подобных ButtonComponent, — это мощный паттерн для структурирования кода игр на Phaser. Вы можете экспериментировать с этим классом: добавить поддержку разных состояний кнопки (активная, неактивная), изменить анимацию нажатия на изменение цвета или прозрачности, реализовать звуковые эффекты или динамическую подстановку текста из конфига. Такой подход значительно ускоряет разработку интерфейсов для ваших игр.
