Статьи

Как сделать компоненты пользовательского интерфейса для игр FlashPunk

Нелегко создать сторону пользовательского интерфейса вашей игры с помощью FlashPunk , так как она по умолчанию не содержит никаких компонентов пользовательского интерфейса, таких как кнопки или текстовые поля. Это не ограничение, хотя; это просто означает, что вы можете свободно создавать пользовательский интерфейс именно так, как вам нравится. Но вам нужно знать, как это сделать в первую очередь! Из этого туториала вы узнаете, как создавать некоторые пользовательские компоненты пользовательского интерфейса, и покажете, как вы можете настроить их в соответствии со стилем вашей игры.


Давайте посмотрим на конечный результат, к которому мы будем стремиться:


Многие пользователи FlashPunk, как правило, испытывают проблемы с интерфейсом своих игр. Не существует «простого» способа создания кнопок и других интерактивных элементов, таких как текстовые поля или флажки, например, в FlashPunk. Нет кнопки класса.

Это может показаться ограничением, да, и многие новички на первых порах находят это немного запутанным … но есть причина этого. Игры, каждая из которых, как правило, имеют уникальный пользовательский интерфейс, дизайн которого связан с механикой или темой самой игры. Вы не увидите ни одной хорошей игры (вообще говоря), в которой есть кнопки по умолчанию для управления!

Мы можем утверждать, что в этом случае могут существовать некоторые классы, обеспечивающие базовую функциональность, минимум, необходимый для работы некоторых кнопок, и оставляющие графическую сторону пользователю … да, это правда. … но обобщение, которое мы должны были бы использовать в этих классах, было бы либо слишком большим и запутанным, либо слишком конкретным и недостаточно затратным. (Поверьте мне, я отвечал за полу-неудачный полуработающий проект Punk.UI.) Вместо этого мы научимся кодировать наши собственные компоненты!

Итак, этот урок покажет вам, как создать свои собственные элементы пользовательского интерфейса для вашей игры во FlashPunk, дать им поведение и показать некоторые приемы, чтобы сделать их графическую часть наиболее используемыми методами … но помните, что каждый из ваших играм понадобится другой графический интерфейс!

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

Для кодирования примера SWF с нашим пользовательским интерфейсом мы собираемся использовать FlashPunk, так как это учебное пособие по FlashPunk, и FlashDevelop для IDE. Если вы чувствуете себя более комфортно с другой IDE, такой как FlashBuilder, вы можете использовать ее, просто адаптировав конкретные части этого руководства.

Предыдущие знания о FlashPunk необходимы, чтобы следовать этому руководству. Это не общий учебник по FlashPunk, это учебник по пользовательскому интерфейсу с использованием FlashPunk. Если вы не знаете FlashPunk, вы можете взглянуть на вводные учебные пособия по FlashPunk в Activetuts +, а также ознакомиться с официальными учебными пособиями по FlashPunk .


Откройте FlashDevelop и нажмите «Проект»> «Новый проект», чтобы открыть окно «Новый проект».

FlashDevelop Новое окно проекта

Выберите ActionScript 3> Проект AS3. В качестве названия проекта укажите «FlashPunk UI». Для определения местоположения щелкните и перейдите в папку, в которую вы хотите сохранить ее. Оставьте флажок «создать каталог для проекта» установленным и нажмите «ОК».


Страница загрузок FlashPunk

Перейдите на официальную веб-страницу FlashPunk, FlashPunk.net , и нажмите кнопку «Загрузить» на панели навигации. Затем нажмите ссылку «Загрузить FlashPunk» в верхней части страницы. Это должно привести вас на страницу загрузки GitHub. Нажмите кнопку «Загрузить .zip» (или «Загрузить .tar.gz», если вы предпочитаете этот формат) и сохраните файл в том месте, где вы его знаете.


Добавлен FlashPunk в наш проект

Теперь, когда мы загрузили FlashPunk, мы должны добавить его в наш проект. Для этого мы просто скопируем папку «net» из zip-файла FlashPunk в нашу папку «src» в нашем проекте, как обычно.


Теперь нам нужно инициализировать FlashPunk в нашем классе документов, чтобы он контролировал нашу игру, как и в любом проекте FlashPunk. Мы делаем это, заставляя наш класс документа расширять класс Engine и вызывая super с необходимыми параметрами. Мы дадим нашему приложению размер 550х400 пикселей. Не забудьте щелкнуть правой кнопкой мыши на нашем проекте, перейти в Свойства и настроить размеры также на 550×400 пикселей.

01
02
03
04
05
06
07
08
09
10
11
12
package
{
    import net.flashpunk.Engine;
     
    public class Main extends Engine
    {
        public function Main():void
        {
            super(550, 400);
        }
    }
}
Настройка размеров нашего проекта

Теперь нам нужно создать мир FlashPunk для хранения наших сущностей. Мы разместим там все компоненты пользовательского интерфейса, чтобы мы могли их протестировать. В реальном мире каждое из наших состояний меню будет различным миром, а также самой игрой. Поэтому мы создадим новый класс, который будет расширять World под названием TestWorld , в нашей папке «src».

01
02
03
04
05
06
07
08
09
10
11
12
13
14
package
{
    import net.flashpunk.World;
     
    public class TestWorld extends World
    {
         
        public function TestWorld()
        {
            super();
        }
         
    }
}

Затем мы сообщим нашему классу Engine при запуске переместиться в наш TestWorld , переопределив функцию init . Не забудьте импортировать класс FP из net.flashpunk !

1
2
3
4
5
6
override public function init():void
{
    super.init();
     
    FP.world = new TestWorld;
}

Первое, что мы собираемся построить, это наш компонент Button. Каждый компонент, который мы собираемся сделать, будет Entity , так как это самый логичный шаг для создания чего-то во FlashPunk, которое будет жить в World .

Прежде всего, мы создадим новую папку внутри папки «src», чтобы все было немного организованно. Мы назовем эту папку «ui» и будем хранить все наши компоненты.

Затем мы создаем класс с именем Button, который будет расширять Entity . Пакет будет пользовательским ui , так как он находится внутри папки пользовательского интерфейса.

01
02
03
04
05
06
07
08
09
10
11
12
package ui
{
    import net.flashpunk.Entity;
     
    public class Button extends Entity
    {
        public function Button(x:Number=0, y:Number=0)
        {
            super(x, y);
        }
    }
}

Теперь мы добавим новый экземпляр этого класса Button в наш Мир, чтобы мы могли видеть, как он работает … когда мы закончим, так как в данный момент это невидимая сущность. Итак, добавьте это в наш класс TestWorld и не забудьте импортировать класс Button с помощью import ui.Button;

1
2
3
4
5
6
override public function begin():void
{
    super.begin();
     
    add(new Button(10, 10));
}

Чтобы сделать нашу кнопку видимой, ей понадобится рисунок. Я создал изображение для нашей кнопки, которое вы можете свободно использовать, но вы также можете создать свою собственную графику, если хотите:

Кнопка графическая

Сохраните его как «button.png» без запятых в новой папке с именем «gfx», расположенной в папке ресурсов, которая будет находиться в корне вашего проекта (на том же уровне, что и папка src).

Теперь нам нужно вставить эту графическую кнопку в нашу игру. Для этого мы вставим изображение, а затем скажем FlashPunk использовать его в качестве нашей графики. Чтобы держать вещи немного организованными, мы будем встраивать все, что нам нужно, в новый класс под названием Assets. Это то, что я склонен делать на всех своих проектах, и это работает как шарм! Итак, мы приступим к созданию нового класса Assets и вставим графику в качестве общедоступной статической константы, чтобы мы могли получить к ней доступ извне:

1
2
3
4
5
6
7
package
{
    public class Assets
    {
        [Embed(source = «../assets/gfx/button.png»)] public static const BUTTON:Class;
    }
}

Наконец, мы скажем FlashPunk использовать это как изображение нашей кнопки. Мы можем использовать его как изображение или как штамп. Разница в том, что Image будет занимать больше памяти, но позволит нам преобразовывать графику, а Stamp будет использовать меньше памяти, но вообще не допустит никакого преобразования, если только они не будут применены вручную непосредственно к BitmapData . В настоящее время мы будем использовать штамп, поскольку нам пока не нужно преобразовывать кнопку. Добавьте это к нашему классу Button, и не забудьте импортировать Stamp.

1
2
3
4
5
6
public function Button(x:Number=0, y:Number=0)
{
    super(x, y);
     
    graphic = new Stamp(Assets.BUTTON);
}

Если вы протестируете проект сейчас, вы увидите нашу графику в мире, но нажатие на нее ничего не изменит. Мы добавим немного поведения сейчас!


Чтобы заставить Entity реагировать на щелчки мышью на FlashPunk, нам просто нужно: проверить, находится ли мышь над сущностью и проверить, отпустила ли мышь этот кадр. Второй шаг действительно прост, нам просто нужно проверить значение переменной mouseReleased в классе Input , но для другого мы должны выполнить тест столкновения между точкой (координатами мыши) и объектом, и сделать это, нам нужно определить столкновение сущностей. На данный момент мы будем использовать хитбокс, так как это самый простой вариант.

Итак, вот код достижения всего того, что мы только что сказали, с пояснением ниже:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package ui
{
    import net.flashpunk.Entity;
    import net.flashpunk.FP;
    import net.flashpunk.graphics.Stamp;
    import net.flashpunk.utils.Input;
     
    public class Button extends Entity
    {
        public function Button(x:Number=0, y:Number=0)
        {
            super(x, y);
             
            graphic = new Stamp(Assets.BUTTON);
            setHitboxTo(graphic);
        }
         
        override public function update():void
        {
            super.update();
             
            if (collidePoint(x, y, world.mouseX, world.mouseY))
            {
                if (Input.mouseReleased) click();
            }
        }
         
        protected function click():void
        {
            FP.screen.color = Math.random() * 0xFFFFFF;
            trace(«click!»);
        }
    }
}
  • Строка 15: мы устанавливаем для хитбокса сущности наш график, поэтому он будет иметь одинаковую ширину и высоту штампа.
  • Строка 22: мы проверяем, сталкивается ли положение мыши в мире, в котором мы находимся, с сущностью.
  • Строка 24: если мышь отпустила этот кадр, мы вызываем функцию щелчка.
  • Строки 28-32: этот код будет выполнен при нажатии кнопки. Он будет отслеживать сообщение о щелчке и изменять цвет фона, поэтому мы заметили, что кнопка была нажата.

Некоторые из вас могут заметить, что этот класс будет работать лучше, если мы сначала проверим состояние мыши, а затем проверим столкновение, но, поскольку нам нужно будет добавить наведенную графику, нам придется изменить его обратно на это, поэтому мы оставим его сюда.

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


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

Прежде всего, нам нужно сделать разные графики для каждого, так что давайте приступим к работе! Вы можете взять эти два я сделал или просто сделать свой собственный. Поместите их в папку button_hover.png с именами button_hover.png и button_down.png .

Графика наведения кнопки
button_hover.png
Графика наведения кнопки
button_down.png

Затем мы добавим их в наш класс активов.

1
2
3
[Embed(source = «../assets/gfx/button.png»)] public static const BUTTON:Class;
[Embed(source = «../assets/gfx/button_down.png»)] public static const BUTTON_DOWN:Class;
[Embed(source=»../assets/gfx/button_hover.png»)] public static const BUTTON_HOVER:Class;

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
protected var normal:Graphic;
protected var hover:Graphic;
protected var down:Graphic;
 
public function Button(x:Number=0, y:Number=0)
{
    super(x, y);
     
    normal = new Stamp(Assets.BUTTON);
    hover = new Stamp(Assets.BUTTON_HOVER);
    down = new Stamp(Assets.BUTTON_DOWN);
     
    graphic = normal;
     
    setHitboxTo(graphic);
}

Прежде чем сделать графическое поведение, мы хотим сделать что-то еще. Нам нужно отслеживать некоторые данные, которые также решат небольшую проблему: если я нажму кнопку мыши вне кнопки, затем перейду к кнопке и отпущу мышь, она будет обнаружена как щелчок мыши. Мы этого не хотим. Поэтому мы создадим новый защищенный логический объект с именем clicked , который будет просто сообщать нам, была ли мышь первой нажата над кнопкой или нет.

1
2
3
4
5
protected var normal:Graphic;
protected var hover:Graphic;
protected var down:Graphic;
 
protected var clicked:Boolean = false;

Затем мы применим следующие изменения к функции обновления: во-первых, внутри проверки столкновений, если в этом кадре была нажата мышь ( Input.mousePressed ), мы установим для нажатой Input.mousePressed значение true . Затем, при проверке освобожденной кнопки мыши, мы также проверим переменную clicked , поэтому мы будем обнаруживать нажатие кнопки только в том случае, если мышь была нажата над ней ранее. Наконец, за пределами проверки столкновения мы установили для clicked значение false при отпускании мыши.

01
02
03
04
05
06
07
08
09
10
11
12
13
override public function update():void
{
    super.update();
     
    if (collidePoint(x, y, world.mouseX, world.mouseY))
    {
        if (Input.mousePressed) clicked = true;
         
        if (clicked && Input.mouseReleased) click();
    }
             
    if (Input.mouseReleased) clicked = false;
}

Что касается самой графики, мы должны сначала спланировать поведение, которое мы имеем в виду. Когда мышь находится над графиком и она не нажата, мы будем отображать состояние наведения . Если мышь находится над графикой и она нажата, мы отобразим нажатое состояние. Если пользователь нажимает кнопку и, удерживая кнопку мыши нажатой, выводит мышь из коллизии кнопки, мы будем отображать состояние наведения , имитирующее поведение SimpleButton AS3. Во всех остальных случаях мы будем отображать нормальное состояние.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
override public function update():void
{
    super.update();
     
    if (collidePoint(x, y, world.mouseX, world.mouseY))
    {
        if (Input.mousePressed) clicked = true;
         
        if (clicked) graphic = down;
        else graphic = hover;
         
        if (clicked && Input.mouseReleased) click();
    }
    else
    {
        if (clicked) graphic = hover;
        else graphic = normal;
    }
     
    if (Input.mouseReleased) clicked = false;
}

Внутри проверки столкновения кнопок мы также проверяем, нажата ли кнопка мыши в данный момент. Мы проверим это, используя переменную Input.mouseDown , а не переменную Input.mouseDown , потому что мы хотим быть уверенными в том, что мы показываем нажатую графику, когда на кнопку мыши нажимали, а не на кнопку снаружи, а затем перетаскивали сверху этого В случае нажатия мыши, мы будем отображать состояние « вниз» , если нет — состояние наведения , потому что мышь будет над кнопкой, но не будет нажата.

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

Здесь вы можете увидеть, как это должно выглядеть на данный момент, но с вашей собственной графикой:


Если вы думаете об этом, флажок на самом деле не так уж отличается от кнопки. Единственное отличие, кроме графического, состоит в том, что они также имеют состояние, которое определяет, проверены они или нет. Фактически, флажок и кнопка — кнопка, которая остается нажатой до тех пор, пока вы не нажмете на нее снова — одинаковы.

По этой причине создать флажок очень просто, если мы знаем, как создать кнопку. Все, что нам нужно добавить, — это дополнительная переменная, которая переключается при каждом щелчке, и с несколькими дополнительными проверками на графические изменения, где мы определяем, какую версию графики показывать: проверенную или непроверенную.

Я также хотел бы научить вас чему-то дополнительному, пока мы делаем флажок. Для кнопки мы создали графику в разных файлах, но что, если мы хотим, чтобы все состояния были в одной и той же графике? Это довольно просто, нам просто нужно использовать свойство clipRect класса Image, как мы покажем при назначении нашей графики-флажка.

Таким образом, поскольку флажки и кнопки имеют много общего, логично, что мы хотим, чтобы наш класс флажков расширял класс кнопок. Но, если бы мы сделали это с текущим кодом, нам пришлось бы заменить функцию полного обновления, чтобы учесть наше состояние флажка, отмеченное при изменениях графики. Таким образом, перед созданием нашего класса Checkbox мы немного реорганизовали наш класс Button, чтобы нам было проще и чище.


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

Созданная нами функция будет называться changeState и будет принимать один аргумент в виде целого числа — состояние. 0 будет означать нормальный, 1 будет зависать, а 2 — вниз. Поскольку мы можем немного запутаться и легко забыть значение этих чисел (ну, не в этом случае, но этот метод может быть полезен вам в других, более сложных случаях), мы создадим некоторые константы, которые будут содержать эти значения вместо ,

Сначала мы создаем эти константы в нашем классе Button:

1
2
3
protected const NORMAL:int = 0;
protected const HOVER:int = 1;
protected const DOWN:int = 2;

Затем мы заменяем все графические изменения, чтобы вызвать нашу еще не созданную функцию:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
override public function update():void
{
    super.update();
     
    if (collidePoint(x, y, world.mouseX, world.mouseY))
    {
        if (Input.mousePressed) clicked = true;
         
        if (clicked) changeState(DOWN);
        else changeState(HOVER);
         
        if (clicked && Input.mouseReleased) click();
    }
    else
    {
        if (clicked) changeState(HOVER);
        else changeState(NORMAL);
    }
     
    if (Input.mouseReleased) clicked = false;
}

Наконец, мы создаем функцию, используя оператор switch с тремя случаями:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
protected function changeState(state:int = 0):void
{
    switch(state)
    {
        case NORMAL:
            graphic = normal;
            break;
        case HOVER:
            graphic = hover;
            break;
        case DOWN:
            graphic = down;
            break;
    }
}

Теперь мы готовы легко кодировать наш флажок! Но сначала…


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

Порядок файла будет следующим: обычный — зависание — вниз; в верхнем ряду будут непроверенные состояния, а в нижнем — проверенные состояния. Вы можете использовать собственную графику или загрузить эту, которую мы предоставляем. Не забудьте сохранить изображение как checkbox.png в папке gfx .

Чекбокс Графический

Теперь мы добавим его в наш класс Assets, чтобы мы могли его использовать.

1
2
3
4
[Embed(source = «../assets/gfx/button.png»)] public static const BUTTON:Class;
[Embed(source = «../assets/gfx/button_down.png»)] public static const BUTTON_DOWN:Class;
[Embed(source = «../assets/gfx/button_hover.png»)] public static const BUTTON_HOVER:Class;
[Embed(source = «../assets/gfx/checkbox.png»)] public static const CHECKBOX:Class;

Наконец, мы создадим наш класс Checkbox и настроим переменные, содержащие графику. Итак, сначала мы создадим класс Checkbox в папке пользовательского ui и сделаем так, чтобы он расширял наш Button.

01
02
03
04
05
06
07
08
09
10
11
package ui
{
    public class Checkbox extends Button
    {
        public function Checkbox(x:Number=0, y:Number=0)
        {
            super(x, y);
             
        }
    }
}

Затем мы добавим еще три переменные, содержащие проверенную графику. Непроверенная графика будет иметь нормальные переменные, которые мы уже имеем.

1
2
3
4
5
6
7
8
protected var normalChecked:Graphic;
protected var hoverChecked:Graphic;
protected var downChecked:Graphic;
 
public function Checkbox(x:Number=0, y:Number=0)
{
    super(x, y);
}

А теперь мы clipRect их как Изображения, чтобы мы могли их обрезать, используя переменную clipRect . Эта переменная принимает Flash Rectangle , который является классом со свойствами x , y , width и height . Таким образом, когда Image видит, что мы предоставляем clipRect, оно будет обрезаться, используя эту информацию. Вот так это будет выглядеть в моем случае. Возможно, вам придется адаптировать значения, чтобы они соответствовали вашим собственным графическим измерениям:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
public function Checkbox(x:Number=0, y:Number=0)
{
    super(x, y);
     
    normal = new Image(Assets.CHECKBOX, new Rectangle(0, 0, 38, 34));
    hover = new Image(Assets.CHECKBOX, new Rectangle(38, 0, 38, 34));
    down = new Image(Assets.CHECKBOX, new Rectangle(76, 0, 38, 34));
     
    normalChecked = new Image(Assets.CHECKBOX, new Rectangle(0, 34, 38, 34));
    hoverChecked = new Image(Assets.CHECKBOX, new Rectangle(38, 34, 38, 34));
    downChecked = new Image(Assets.CHECKBOX, new Rectangle(76, 34, 38, 34));
     
    graphic = normal;
    setHitboxTo(normal);
}

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


Теперь мы добавим флажок в наш Мир и посмотрим, как он выглядит! Добавьте это в наш класс TestWorld:

1
2
3
4
5
6
7
override public function begin():void
{
    super.begin();
     
    add(new Button(10, 10));
    add(new Checkbox(20, 140));
}

Теперь протестируйте проект … эй, подождите! Наш флажок действует как обычная кнопка, он не проверяет и не снимает сам! Мы еще не добавили поведение, вот что мы собираемся сделать сейчас.


Прежде всего, нам нужно создать публичное логическое значение, которое будет содержать проверенное состояние нашей кнопки. Это будет называться … о, сюрприз! checked Так…

1
2
3
4
5
protected var normalChecked:Graphic;
protected var hoverChecked:Graphic;
protected var downChecked:Graphic;
 
public var checked:Boolean = false;

Теперь нам нужно будет переключать эту переменную каждый раз, когда мы нажимаем флажок. Самый простой способ сделать это — переопределить функцию щелчка и переключить ее, поэтому добавьте это в наш флажок:

1
2
3
4
5
6
override protected function click():void
{
    checked = !checked;
     
    super.click();
}

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
override protected function changeState(state:int = 0):void
{
    if (checked)
    {
        switch(state)
        {
            case NORMAL:
                graphic = normalChecked;
                break;
            case HOVER:
                graphic = hoverChecked;
                break;
            case DOWN:
                graphic = downChecked;
                break;
        }
    }
    else
    {
        super.changeState(state);
    }
}

Итак, сначала мы проверим проверенное свойство. Если это правда, мы проверяем состояние, в которое мы только что изменили, и устанавливаем его в соответствующий проверенный рисунок. В противном случае, если флажок не установлен, мы вызываем нашу версию ButtonState changeState, которая просто устанавливает графический элемент в непроверенное состояние. Используя там super , нам нужно написать меньше кода, чтобы сделать то же самое поведение! Ура!

Если мы протестируем проект, то увидим, что флажок работает сейчас. Это должно выглядеть так, но с вашей собственной графикой:


Если подумать, радио-кнопка практически такая же, как флажок. Единственное отличие состоит в том, что, хотя флажок полностью независим от других флажков, RadioButton является частью группы, и в этой группе только ОДИН член может быть проверен одновременно, поэтому нам придется справиться с этим.

Таким образом, вместо того, чтобы открывать и закрывать RadioButton, она сообщает группе, что ее нужно проверить, и группа снимает все остальные переключатели и проверяет наши … и что за группа?

По сути, группа RadioButton будет специальным классом с массивом (в нашем случае вектором AS3), содержащим все кнопки RadioButton, принадлежащие группе. Он также будет содержать методы для добавления и удаления кнопок из группы.

Прежде всего, тем не менее, мы сделаем графику для наших RadioButtons …


Мы сделаем графику для RadioButton так же, как мы делали это для Checkbox. Если хотите, вы можете создать собственную графику, но если нет, то вы можете использовать ее. Сохраните вашу графику как «radiobutton.png» в папке gfx.

RadioButton Graphic

Теперь мы добавим его в наш класс Assets, чтобы мы могли его использовать.

1
2
3
4
5
[Embed(source = «../assets/gfx/button.png»)] public static const BUTTON:Class;
[Embed(source = «../assets/gfx/button_down.png»)] public static const BUTTON_DOWN:Class;
[Embed(source = «../assets/gfx/button_hover.png»)] public static const BUTTON_HOVER:Class;
[Embed(source = «../assets/gfx/checkbox.png»)] public static const CHECKBOX:Class;
[Embed(source = «../assets/gfx/radiobutton.png»)] public static const RADIOBUTTON:Class;

После этого мы создаем наш класс RadioButton, который будет расширять RadioButton, и мы настроим графику так же, как мы делали это раньше.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package ui
{
    import flash.geom.Rectangle;
    import net.flashpunk.graphics.Image;
     
    public class RadioButton extends Checkbox
    {
        public function RadioButton(x:Number=0, y:Number=0)
        {
            super(x, y);
             
            normal = new Image(Assets.RADIOBUTTON, new Rectangle(0, 0, 39, 44));
            hover = new Image(Assets.RADIOBUTTON, new Rectangle(39, 0, 39, 44));
            down = new Image(Assets.RADIOBUTTON, new Rectangle(78, 0, 39, 44));
             
            normalChecked = new Image(Assets.RADIOBUTTON, new Rectangle(0, 44, 39, 44));
            hoverChecked = new Image(Assets.RADIOBUTTON, new Rectangle(39, 44, 39, 44));
            downChecked = new Image(Assets.RADIOBUTTON, new Rectangle(78, 44, 39, 44));
             
            graphic = normal;
            setHitboxTo(normal);
        }
    }
}

Пришло время создать саму группу радиопереключателей. Класс будет содержать все радиокнопки в векторе радиокнопок. Он также будет иметь следующие методы: add (), для добавления нового переключателя, addList (), для добавления нескольких переключателей за один шаг, remove (), для удаления переключателя, removeList (), эквивалент addList (), но для удаления и removeAll (), чтобы удалить все переключатели из группы одновременно. Он также будет иметь метод getAt (), чтобы получить переключатель по индексу, и getIndex (), чтобы получить индекс кнопки. Мы не будем тратить много времени на объяснение этих методов, поскольку они являются основными операциями для массивов.

Затем, при создании RadioButton, он запросит группу переключателей в качестве параметра, и он будет добавлен туда автоматически, если он предусмотрен. Также, при удалении RadioButton из мира, оно также будет удалено из его группы. Наконец, при щелчке он сам по себе ничего не делает, а вызывает внутренний метод группы, который будет вызываться click (). Этот метод снимает все переключатели и проверяет тот, который вызвал метод.

Прежде всего, мы создаем группу. Я предоставил некоторые комментарии, чтобы объяснить несколько вещей, но так как это только основные операции с массивом, я не буду объяснять все это:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
package ui
{
    public class RadioButtonGroup
    {
        public var buttons:Vector.<RadioButton> = new Vector.<RadioButton>;
         
        public function RadioButtonGroup(…buttons)
        {
            //we add the buttons provided to the constructor, if any
            if (buttons) addList(buttons);
        }
         
        public function add(button:RadioButton):void
        {
            buttons.push(button);
        }
         
        public function addList(…buttons):void
        {
            if (!buttons) return;
            if (buttons[0] is Array || buttons[0] is Vector.<RadioButton>)
            {
                //if the parameter is an array or vector of radio buttons, we add the buttons in the vector / array
                for each(var b:RadioButton in buttons[0]) add(b);
            }
            else
            {
                //if the parameters are simply a comma separated list of buttons, we add those instead
                for each(var r:RadioButton in buttons) add(r);
            }
        }
         
        public function remove(button:RadioButton):void
        {
            buttons.splice(getIndex(button), 1);
        }
         
        public function removeList(…buttons):void
        {
            if (!buttons) return;
            if (buttons[0] is Array || buttons[0] is Vector.<RadioButton>)
            {
                //if the parameter is an array or vector of radio buttons, we remove the buttons in the vector / array
                for each(var b:RadioButton in buttons[0]) remove(b);
            }
            else
            {
                //if the parameters are simply a comma separated list of buttons, we remove those instead
                for each(var r:RadioButton in buttons) remove(r);
            }
        }
         
        public function removeAll():void
        {
            //fastest way to clear a vector
            buttons.length = 0;
        }
         
        public function getAt(index:int):RadioButton
        {
            return buttons[index];
        }
         
        public function getIndex(button:RadioButton):int
        {
            return buttons.indexOf(button);
        }
    }
}

Теперь мы заставим наши RadioButton запрашивать группу в конструкторе, и добавим себя в нее, если предоставлено:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
public function RadioButton(x:Number=0, y:Number=0, group:RadioButtonGroup = null)
{
    super(x, y);
     
    if (group) group.add(this);
     
    normal = new Image(Assets.RADIOBUTTON, new Rectangle(0, 0, 39, 44));
    hover = new Image(Assets.RADIOBUTTON, new Rectangle(39, 0, 39, 44));
    down = new Image(Assets.RADIOBUTTON, new Rectangle(78, 0, 39, 44));
     
    normalChecked = new Image(Assets.RADIOBUTTON, new Rectangle(0, 44, 39, 44));
    hoverChecked = new Image(Assets.RADIOBUTTON, new Rectangle(39, 44, 39, 44));
    downChecked = new Image(Assets.RADIOBUTTON, new Rectangle(78, 44, 39, 44));
     
    graphic = normal;
    setHitboxTo(normal);
}

Наконец, мы добавим кнопки на мир, чтобы мы могли проверить их. Мы добавим 3 кнопки в группу и 2 в другую.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
override public function begin():void
{
    super.begin();
     
    add(new Button(10, 10));
    add(new Checkbox(20, 140));
     
    var group1:RadioButtonGroup = new RadioButtonGroup();
    add(new RadioButton(20, 200, group1));
    add(new RadioButton(20, 250, group1));
    add(new RadioButton(20, 300, group1));
     
    var group2:RadioButtonGroup = new RadioButtonGroup();
    add(new RadioButton(200, 200, group2));
    add(new RadioButton(200, 250, group2));
}

Если вы проверите это сейчас, переключатели будут работать как обычные флажки. Пришло время изменить это сейчас.


Прежде всего, нам нужно переопределить функцию щелчка RadioButton, и вместо вызова кода, установленного в флажке с помощью super, мы будем вызывать еще не созданную функцию щелчка нашей группы RadioButton … упс ! Как мы будем вызывать эту функцию, если у нас нет ссылки на группу? Простым решением было бы установить ссылку из группы, предоставленной в конструкторе, но … тогда мы не сможем изменить группы, и все методы, которые мы видим в группе, будут бесполезны.

Вместо этого мы сначала добавим в RadioButton переменную, которая будет ссылаться на его группу, и мы установим это для добавленного метода группы. Мы также установим его в null при удалении кнопки:

1
2
3
4
5
6
public class RadioButton extends Checkbox
{
    internal var group:RadioButtonGroup;
     
    // […] all the methods in the RadioButton class were omitted for brevety.
}

Теперь мы добавим это к следующим методам группы RadioButton:

1
2
3
4
5
public function add(button:RadioButton):void
{
    button.group = this;
    buttons.push(button);
}
1
2
3
4
5
public function remove(button:RadioButton):void
{
    button.group = null;
    buttons.splice(getIndex(button), 1);
}

И мы также установим все групповые ссылки на null в функции removeAll:

1
2
3
4
5
6
public function removeAll():void
{
    for each(var b:RadioButton in buttons) b.group = null;
    //fastest way to clear a vector
    buttons.length = 0;
}

Теперь мы готовы вызвать функцию щелчка на RadioButton!

1
2
3
4
override protected function click():void
{
    group.click(this);
}

И, наконец, мы создадим функцию щелчка. Эта функция снимает все RadioButtons в группе, а затем проверяет предоставленную кнопку.

1
2
3
4
5
internal function click(target:RadioButton):void
{
    for each(var b:RadioButton in buttons) b.checked = false;
    target.checked = true;
}

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

1
2
3
4
5
6
override public function removed():void
{
    super.removed();
     
    group.remove(this);
}

Вот так это будет выглядеть, но с вашей собственной графикой!


Что если мы хотим, чтобы наши кнопки содержали текст? Например, в меню нашей игры нам может понадобиться кнопка «Воспроизвести», кнопка «Справка» и т. Д., И мы хотим, чтобы на этих кнопках был какой-то текст, который сообщает, какая из них является игрой, а какая из них помощь. В настоящее время единственный способ сделать это — создать новое изображение для каждого из них, а также создать новый класс и изменить изображение в классе …

Поэтому мы можем принять дополнительный параметр в наших компонентах: текст. Это будет строка, которую мы отправим нашим компонентам, а затем они отобразят ее в виде текста. Супер просто!

Давайте реализуем это в нашей кнопке сейчас. Сначала мы добавим текст параметра в виде строки, а также переменную метки с типом Text, который представляет собой изображение FlashPunk:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
protected var label:Text;
 
public function Button(x:Number=0, y:Number=0, text:String = «»)
{
    super(x, y);
     
    normal = new Stamp(Assets.BUTTON);
    hover = new Stamp(Assets.BUTTON_HOVER);
    down = new Stamp(Assets.BUTTON_DOWN);
     
    graphic = normal;
     
    setHitboxTo(graphic);
}

Затем мы создадим экземпляр этого текста размером 16 пикселей и черным цветом с шириной кнопки (без границ), активированной wordWrap и выровненной по центру.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
public function Button(x:Number=0, y:Number=0, text:String = «»)
{
    super(x, y);
     
    var normalStamp:Stamp = new Stamp(Assets.BUTTON);
     
    label = new Text(text, 10, 0, { size: 16, color: 0x000000, width: normalStamp.width — 30, wordWrap: true, align: «center» } );
     
    normal = normalStamp;
    hover = new Stamp(Assets.BUTTON_HOVER);
    down = new Stamp(Assets.BUTTON_DOWN);
     
    graphic = normal;
     
    setHitboxTo(normalStamp);
}

Мы также создали дополнительную переменную, Stamp, которая будет содержать штамп нормального изображения, чтобы мы могли ссылаться на его ширину. Мы устанавливаем нормальную графику на normalStamp.

Теперь нам нужно отобразить этот текст где-нибудь. Для этого нам нужно включить его в изображение нашей кнопки. В FlashPunk, чтобы отобразить более одной графики одновременно, нам нужно поместить их все в графический список и использовать вместо этого в качестве графики. Поэтому мы сделаем нечто подобное, чтобы все наши графики содержали текст:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
public function Button(x:Number=0, y:Number=0, text:String = «»)
{
    super(x, y);
     
    var normalStamp:Stamp = new Stamp(Assets.BUTTON);
     
    label = new Text(text, 10, 0, { size: 16, color: 0x000000, width: normalStamp.width — 30, wordWrap: true, align: «center» } );
     
    normal = new Graphiclist(normalStamp, label);
    hover = new Graphiclist(new Stamp(Assets.BUTTON_HOVER), label);
    down = new Graphiclist(new Stamp(Assets.BUTTON_DOWN), label);
     
    graphic = normal;
     
    setHitboxTo(normalStamp);
}

Наконец, мы будем центрировать наш текст по вертикали:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
public function Button(x:Number=0, y:Number=0, text:String = «»)
{
    super(x, y);
     
    var normalStamp:Stamp = new Stamp(Assets.BUTTON);
     
    label = new Text(text, 10, 0, { size: 16, color: 0x000000, width: normalStamp.width — 30, wordWrap: true, align: «center» } );
    label.y = (normalStamp.height — label.textHeight) * 0.5;
     
    normal = new Graphiclist(normalStamp, label);
    hover = new Graphiclist(new Stamp(Assets.BUTTON_HOVER), label);
    down = new Graphiclist(new Stamp(Assets.BUTTON_DOWN), label);
     
    graphic = normal;
     
    setHitboxTo(normalStamp);
}

Так что, если мы добавим некоторый текст в кнопку, которую мы создали в нашем TestWorld, мы увидим что-то вроде этого:


В настоящее время существует небольшая проблема с тем, как мы работаем с текстом: нам нужно создать новый Graphiclist для каждой графики. Это означает, что мы должны написать больше кода для КАЖДОГО графического объекта, который у нас есть, и у нас не может быть текста в классах, который расширяет нас, если мы не изменим это. Немного неудобно.

Итак, вот мое решение: вместо вставки текста в нашу графику мы просто отрендерим его вручную, переопределив функцию рендеринга.

Вот созданная мной функция renderGraphic, которая имитирует рендеринг класса Entity, но принимает графику в качестве параметра. Мы добавим это к нашей кнопке:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
protected function renderGraphic(graphic:Graphic):void
{
    if (graphic && graphic.visible)
    {
        if (graphic.relative)
        {
            _point.x = x;
            _point.y = y;
        }
        else _point.x = _point.y = 0;
        _camera.x = world ?
        _camera.y = world ?
        graphic.render(renderTarget ? renderTarget : FP.buffer, _point, _camera);
    }
}
protected var _point:Point = FP.point;
protected var _camera:Point = FP.point2;

Теперь мы установим графику, как она была раньше:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
public function Button(x:Number=0, y:Number=0, text:String = «»)
{
    super(x, y);
     
    var normalStamp:Stamp = new Stamp(Assets.BUTTON);
     
    label = new Text(text, 10, 0, { size: 16, color: 0x000000, width: normalStamp.width — 30, wordWrap: true, align: «center» } );
    label.y = (normalStamp.height — label.textHeight) * 0.5;
     
    normal = normalStamp;
    hover = new Stamp(Assets.BUTTON_HOVER);
    down = new Stamp(Assets.BUTTON_DOWN);
     
    graphic = normal;
     
    setHitboxTo(normalStamp);
}

И, наконец, мы переопределим функцию рендеринга для рендеринга текста и графического изображения кнопки:

1
2
3
4
5
6
override public function render():void
{
    super.render();
     
    renderGraphic(label);
}

Пришло время добавить текст в наш флажок! Прежде всего, мы должны принять текст в виде строки. Затем, поскольку мы хотим, чтобы он был совершенно другим (ширина / высота и цвет), мы заменим метку следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
public function Checkbox(x:Number=0, y:Number=0, text:String = «»)
{
    super(x, y, text);
     
    normal = new Image(Assets.CHECKBOX, new Rectangle(0, 0, 38, 34));
    hover = new Image(Assets.CHECKBOX, new Rectangle(38, 0, 38, 34));
    down = new Image(Assets.CHECKBOX, new Rectangle(76, 0, 38, 34));
     
    normalChecked = new Image(Assets.CHECKBOX, new Rectangle(0, 34, 38, 34));
    hoverChecked = new Image(Assets.CHECKBOX, new Rectangle(38, 34, 38, 34));
    downChecked = new Image(Assets.CHECKBOX, new Rectangle(76, 34, 38, 34));
     
    label = new Text(text, 40, 0, { color: 0xFFFFFF, size: 16 } );
    label.y = (Image(normal).height — label.textHeight) * 0.5;
     
    graphic = normal;
    setHitboxTo(normal);
}

И, благодаря возможности расширения, чтобы иметь текст в наших RadioButtons, нам нужно только принять текстовый параметр и отправить его в super (и центрировать его вертикально)!

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
public function RadioButton(x:Number=0, y:Number=0, group:RadioButtonGroup = null, text:String = "")
{
    super(x, y, text);
     
    if (group) group.add(this);
     
    normal = new Image(Assets.RADIOBUTTON, new Rectangle(0, 0, 39, 44));
    hover = new Image(Assets.RADIOBUTTON, new Rectangle(39, 0, 39, 44));
    down = new Image(Assets.RADIOBUTTON, new Rectangle(78, 0, 39, 44));
     
    normalChecked = new Image(Assets.RADIOBUTTON, new Rectangle(0, 44, 39, 44));
    hoverChecked = new Image(Assets.RADIOBUTTON, new Rectangle(39, 44, 39, 44));
    downChecked = new Image(Assets.RADIOBUTTON, new Rectangle(78, 44, 39, 44));
     
    label.y = (Image(normal).height - label.textHeight) * 0.5;
     
    graphic = normal;
    setHitboxTo(normal);
}

В некоторых приложениях и пользовательских интерфейсах «хитбокс» флажка / radiobutton также включает его текст, поэтому мы можем включить его с помощью простой строки в классе Checkbox в конце конструктора (и в RadioButton также):

1
width = label.x + label.textWidth;

Если мы добавим текст в наш TestWorld и протестируем проект, у нас будет что-то вроде этого:


Так что теперь мы можем легко создавать разные кнопки с разным текстом, чтобы различать их … но в данный момент все наши кнопки — это изменение цвета фона. Бесполезно иметь миллионы кнопок, если все они … ну, просто измените цвет фона. Если бы мы хотели иметь разные функциональные возможности с текущим кодом, то нам нужно было бы создать класс Button для каждой функциональности, которую будет выполнять кнопка … это нет-нет!

Вместо этого мы будем использовать очень похожий подход к текстовой задаче: мы добавим параметр для функциональности. Этот параметр будет просто функцией. Мы отправим ему имя функции, и когда кнопка будет нажата, функция будет вызвана. Таким образом, мы можем иметь в нашем Мире три функции: gotoGame, gotoOptions и gotoCredits. Затем мы можем связать каждого из них с кнопкой Play, кнопкой Options и кнопкой Credits.

Делать это очень легко. Нам просто нужно принять параметр обратного вызова, сохранить его в наших переменных Button и вызвать его в функции click. Ура! Мы также удалим код цвета фона.

Часть переменных класса кнопки и часть ее конструктора:

01
02
03
04
05
06
07
08
09
10
11
//[…]
protected var label:Text;
public var callback:Function;
 
public function Button(x:Number=0, y:Number=0, text:String = "", callback:Function = null)
{
    super(x, y);
     
    this.callback = callback;
    //[…]
}

И функция щелчка, вызывающая только обратный вызов, если был предоставлен обратный вызов:

1
2
3
4
protected function click():void
{
    if (callback != null) callback();
}

Вот и все … но я хочу добавить дополнительную функцию для флажков. Весь смысл создания флажка заключается в том, чтобы определить, установлен он или нет. Текущий способ сделать это — сохранить ссылку на флажок в классе World, а затем проверить его свойство clicked в обратном вызове … но немного утомительно иметь ссылки на каждый флажок в нашем мире, если у нас много их. Итак, что мы собираемся сделать, это: отправить логический параметр для обратного вызова со значением свойства selected. Таким образом, для обратного вызова флажка мы должны принять логическое значение, значением которого будет проверенное значение флажка. Ницца!

Вот новая функция щелчка флажка:

1
2
3
4
5
6
override protected function click():void
{
    checked = !checked;
     
    if (callback != null) callback(checked);
}

Не забудьте также запросить параметр обратного вызова и отправить его в super, как показано в этом фрагменте конструктора Checkbox:

1
2
3
4
5
public function Checkbox(x:Number=0, y:Number=0, text:String = "", callback:Function = null)
{
    super(x, y, text, callback);
    //[…]
}

А в случае с радиокнопками, я думаю, нам нужен другой подход. Обычно с радиокнопкой вы не хотите определять щелчок каждой отдельной радиокнопки … что вы хотите сделать, это определить, какая кнопка нажата в группе. Вот почему я думаю, что обратный вызов должен быть не на самой кнопке, а в группе RadioButton. Кроме того, мы также хотим, чтобы этот обратный вызов предоставил информацию, чтобы определить, какая кнопка радио была нажата. Это можно легко отсортировать, потребовав дополнительный параметр в RadioButton: идентификатор и отправив его в обратный вызов.

Итак, прежде всего, мы добавляем параметр id для радиокнопок, как показано в этом фрагменте конструктора RadioButton.

1
2
3
4
5
6
7
8
9
public var id:String = "";
 
public function RadioButton(x:Number=0, y:Number=0, group:RadioButtonGroup = null, text:String = "", id:String = "")
{
    super(x, y, text);
     
    this.id = id;
    //[…]
}

Затем мы отправляем его группе в функции щелчка.

1
2
3
4
override protected function click():void
{
    group.click(this, id);
}

Теперь очередь за RadioButtonGroup. Сначала добавим параметр обратного вызова:

1
2
3
4
5
6
7
8
9
public var callback:Function = null
 
public function RadioButtonGroup(callback:Function = null, ...buttons)
{
    this.callback = callback;
     
    //we add the buttons provided to the constructor, if any
    if (buttons) addList(buttons);
}

Затем мы вызываем его с помощью идентификатора, предоставленного при нажатии кнопки RadioButton из группы:

1
2
3
4
5
6
7
internal function click(target:RadioButton, id:String):void
{
    if (callback != null) callback(id);
     
    for each(var b:RadioButton in buttons) b.checked = false;
    target.checked = true;
}

Это оно!


Пользовательское восприятие параметров! В основном, скажем, у нас есть игра с 30 уровнями. Мы хотим создать Экран Уровня, где пользователь может выбрать уровень, на котором он хочет играть. Если бы нам нужно было создать функцию для перехода на каждый уровень, было бы больно … действительно проще, если бы мы могли передать номер уровня обратному вызову! Таким образом, мы назначаем номер уровня при создании кнопки, возможно, из цикла, а затем обратный вызов считывает номер и показывает нам соответствующий уровень.

Если бы мы запросили дополнительный аргумент, называемый номером уровня, это решение не сработало бы для любого другого случая, когда нам нужны дополнительные параметры. Вместо этого мы будем запрашивать необязательный параметр Object. Как это будет объект, он может быть INT, строка, пользовательский класс, даже массив содержит несколько Params … или сам объект, в этом синтаксисе: {param: "value", param2: 1, param3: false}. Таким образом, мы можем иметь несколько параметров , которые можно получить следующим образом: object.param, object.param2и т.д.

Реализовать это действительно просто. Нам просто нужно запросить дополнительный параметр, call paramsи отправить его в обратный вызов. Вы должны быть в состоянии сделать это самостоятельно, но на всякий случай вот код для Button и Checkbox:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
protected var label:Text;
public var callback:Function;
public var params:Object;
 
public function Button(x:Number=0, y:Number=0, text:String = "", callback:Function = null, params:Object = null)
{
    super(x, y);
     
    this.callback = callback;
    this.params = params;
    //[…]
}
 
//[…]
 
protected function click():void
{
    if (callback != null)
    {
        if (params != null) callback(params);
        else callback();
    }
}

А теперь флажок:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
public function Checkbox(x:Number=0, y:Number=0, text:String = "", callback:Function = null, params:Object = null)
{
    super(x, y, text, callback, params);
     
    //[…]
}
 
override protected function click():void
{
    checked = !checked;
     
    if (callback != null)
    {
        if (params != null) callback(checked, params);
        else callback(checked);
    }
}

Теперь для RadioButton мы хотим, чтобы параметры были в кнопке, но они будут переданы группе и отправлены с помощью группового обратного вызова … подождите минуту! У нас уже есть то, что просто так! Но это называется ID и это строка. Я думаю, мы можем с уверенностью сказать, что мы можем изменить ID на params и установить его в объект, и мы сможем использовать его по той же причине, что и ID, а также в качестве дополнительных параметров.

По сути, нам нужно удалить переменную id в RadioButton и переименовать параметр в конструкторе:

1
2
3
4
5
6
public function RadioButton(x:Number=0, y:Number=0, group:RadioButtonGroup = null, text:String = "", params:Object = null)
{
    super(x, y, text, null, params);
     
    //[…]
}

Затем мы отправляем параметры на функцию щелчка вместо идентификатора:

1
2
3
4
override protected function click():void
{
    group.click(this, params);
}

И мы изменили функцию щелчка по группе, отражающую также эти изменения:

1
2
3
4
5
6
7
internal function click(target:RadioButton, params:Object):void
{
    if (callback != null) callback(params);
     
    for each(var b:RadioButton in buttons) b.checked = false;
    target.checked = true;
}

Текстовые входы (текстовые поля) обычно используются в играх. Большинству игр требуется по крайней мере один текстовый ввод от пользователя: чтобы установить его имя для оценки. В зависимости от игры могут быть другие способы ввода текста. Теперь мы собираемся научиться делать их во FlashPunk, не используя AS3 TextField.

Обратите внимание, что, реализуя TextInput непосредственно в FlashPunk, есть некоторые символы, такие как акценты, которые не могут быть напечатаны. Если вам это нужно, вы можете добавить AS3 TextFields в FlashPunk напрямую, добавив их на stage ( FP.stage.addChild) или в движок MovieClip ( FP.engine.addChild). В этом нет необходимости, если вы используете английский текст, но на других языках они есть, так что имейте это в виду.

Прежде всего, мы создадим компонент TextInput. Класс расширит Entity и будет иметь два пользовательских свойства: text как String и textGraphic типа Text. text будет содержать текст, введенный в TextInput, а textGraphic будет его визуальным представлением. Мы реализуем текст как получатель / установщик, потому что когда мы изменяем его с помощью кода, мы также хотим обновить визуальное представление.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package ui
{
    import net.flashpunk.Entity;
    import net.flashpunk.graphics.Text;
     
    public class TextInput extends Entity
    {
        protected var _text:String = "";
        protected var textGraphic:Text;
         
        public function TextInput(x:Number=0, y:Number=0, text:String = "")
        {
            super(x, y);
             
            textGraphic = new Text("", 0, 0, { size: 16, color: 0xFFFFFF } );
            this.text = text;
            graphic = textGraphic;
        }
         
        public function get text():String
        {
            return _text;
        }
         
        public function set text(value:String):void
        {
            _text = value;
            textGraphic.text = value;
        }
    }
}

Теперь нам нужно проверить каждый ключ и записать их в текстовую строку … или использовать действительно полезную переменную, предоставленную в классе Input! Переменная называется keyString и регистрирует последние 100 ключей, которые были написаны пользователем. Таким образом, мы можем получить строку, чтобы узнать, что набрал пользователь. Самый простой способ — добавить функцию обновления в наше textField, добавить все ключи переменной в наш текст и очистить переменную, чтобы мы не дублировали ключи, которые мы уже получили.

01
02
03
04
05
06
07
08
09
10
override public function update():void
{
    super.update();
     
    if (Input.keyString != "")
    {
        text += Input.keyString;
        Input.keyString = "";
    }
}

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

01
02
03
04
05
06
07
08
09
10
11
12
override public function update():void
{
    super.update();
     
    if (Input.keyString != "")
    {
        text += Input.keyString;
        Input.keyString = "";
    }
     
    if (Input.pressed(Key.BACKSPACE)) text = _text.substr(0, _text.length - 1);
}

Проблема в том, что этот метод не будет удален, если пользователь удерживает нажатой клавишу возврата. Для этого мы будем использовать KeyboardEvent, который добавим на сцену. Мы также удалим событие при удалении ввода текста, и оно ничего не сделает, если обнаружит, что текущий мир — это не мир, в котором находится текст.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
override public function added():void
{
    super.added();
     
    FP.stage.addEventListener(KeyboardEvent.KEY_DOWN, onKeyDown);
}
 
protected function onKeyDown(e:KeyboardEvent):void
{
    if (world != FP.world) return;
    if (e.keyCode != Key.BACKSPACE) return;
     
    text = _text.substr(0, _text.length - 1);
}
 
override public function removed():void
{
    super.removed();
     
    FP.stage.removeEventListener(KeyboardEvent.KEY_DOWN, onKeyDown);
}

Отсюда, добавление мультилинии действительно просто. Нам просто нужно проверить ключ Enter на onKeyDownфункции и вставить "\n"символ в строку:

1
2
3
4
5
6
7
protected function onKeyDown(e:KeyboardEvent):void
{
    if (world != FP.world) return;
     
    if (e.keyCode == Key.BACKSPACE) text = _text.substr(0, _text.length - 1);
    else if (e.keyCode == Key.ENTER) text += "\n";
}

Мы могли бы даже добавить логическое значение, чтобы разрешить или нет многострочный. Таким образом, мы можем иметь «текстовые поля» и «текстовые поля». Сначала мы добавляем его в наши переменные и запрашиваем его как необязательный параметр для конструктора:

01
02
03
04
05
06
07
08
09
10
11
12
protected var multiline:Boolean = false;
 
public function TextInput(x:Number=0, y:Number=0, text:String = "", multiline:Boolean = false)
{
    super(x, y);
     
    this.multiline = multiline;
     
    textGraphic = new Text("", 0, 0, { size: 16, color: 0xFFFFFF } );
    this.text = text;
    graphic = textGraphic;
}

Затем мы просто добавляем проверку в функцию onKeyDown.

1
2
3
4
5
6
7
protected function onKeyDown(e:KeyboardEvent):void
{
    if (world != FP.world) return;
     
    if (e.keyCode == Key.BACKSPACE) text = _text.substr(0, _text.length - 1);
    else if (e.keyCode == Key.ENTER && multiline) text += "\n";
}

Теперь, если вы добавите многострочное текстовое поле в свой мир, это примерно то, что вы увидите:


Если вы попытаетесь добавить два TextInputs на данный момент, вы увидите, что обычные символы будут работать только на самом последнем добавленном TextField, в то время как стирание и ключи новой строки будут работать на всех них одновременно. Мы хотим добиться того, чтобы пользователь мог вводить только одно текстовое поле одновременно.

Это может быть легко достигнуто путем создания некоторой системы «фокусировки». Сначала ни одно текстовое поле не будет сфокусировано, поэтому ввод текста ничего не даст. Когда пользователь щелкает текстовое поле, текстовое поле, на которое нажали, будет сфокусировано, поэтому ввод будет работать ТОЛЬКО в этом текстовом поле. Если затем мы нажмем еще один, ввод будет работать ТОЛЬКО на вновь выбранном. Если затем мы щелкнем куда-нибудь без текстовых полей, мы уберем фокус с ранее выбранного, поэтому при наборе текста ничего больше не будет.

Есть два простых способа решить эту проблему. Один предполагает наличие пользовательского класса World, другой — статическую переменную. Поскольку создание нового мирового класса, который необходимо будет использовать для создания текстовых входов, немного хлопотно, мы перейдем к подходу со статическими переменными. В нашем классе TextInput будет статическая переменная, которая будет содержать фокусированный TextInput. При проверке типа мы прежде проверим, являемся ли мы сфокусированным TextInput, если нет, мы просто проигнорируем типизацию. Более того, при обнаружении щелчка по текстовому полю мы установим эту переменную для себя. Наконец, мы создадим некоторую систему для отмены ввода текста, но об этом мы поговорим позже.

Сначала мы создаем статическую переменную в классе TextInput:

1
public static var focus:TextInput;

Затем мы добавляем проверки в функцию обновления:

01
02
03
04
05
06
07
08
09
10
override public function update():void
{
    super.update();
     
    if (TextInput.focus == this && Input.keyString != "")
    {
        text += Input.keyString;
        Input.keyString = "";
    }
}

И проверки для onKeyDownфункции также:

1
2
3
4
5
6
7
8
protected function onKeyDown(e:KeyboardEvent):void
{
    if (world != FP.world) return;
    if (TextInput.focus != this) return;
     
    if (e.keyCode == Key.BACKSPACE) text = _text.substr(0, _text.length - 1);
    else if (e.keyCode == Key.ENTER && multiline) text += "\n";
}

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

1
2
3
4
5
6
7
public function set text(value:String):void
{
    _text = value;
    textGraphic.text = value;
     
    setHitbox(textGraphic.textWidth, textGraphic.textHeight);
}

Затем мы проверяем клик и устанавливаем фокус на нас, если он есть:

01
02
03
04
05
06
07
08
09
10
11
12
override public function update():void
{
    super.update();
     
    if (Input.mousePressed && collidePoint(x, y, world.mouseX, world.mouseY)) TextInput.focus = this;
     
    if (TextInput.focus == this && Input.keyString != "")
    {
        text += Input.keyString;
        Input.keyString = "";
    }
}

Добавьте еще один текстовый ввод в мир и попробуйте что-нибудь напечатать. Ничего не случится. Теперь выберите один из них и введите, вы увидите, что ввод, стирание и ввод (если многострочный режим включен) будет работать только для выбранного ввода текста. Вы также можете переключать их.


В нашем TextInput есть небольшая ошибка. Откройте файл SWF, введите что-нибудь, не выделив текстового ввода, а затем щелкните один из них Пуф! Призрак оставил сообщение для вас!

Не волнуйся, призрака нет. Проблема в том, что мы удаляем keyString из Input при обновлении, только если выбрано текстовое поле. Таким образом, если выбора нет, мы его не удаляем, а когда мы выбираем новое текстовое поле, появляются сразу все последние 100 введенных символов.

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
override public function update():void
{
    super.update();
     
    if (Input.mousePressed && collidePoint(x, y, world.mouseX, world.mouseY))
    {
        TextInput.focus = this;
        Input.keyString = "";
    }
     
    if (TextInput.focus == this && Input.keyString != "")
    {
        text += Input.keyString;
        Input.keyString = "";
    }
}

Следующая вещь: не выбрав. Чтобы отменить выбор, мы сделаем дополнительную проверку, если щелкнуть мышью, а TextInput не был выбран: мы проверим, есть ли TextInput под мышью. Если нет, мы устанавливаем фокус на ноль.

Для этого мы установим нашу typeсобственность. Если вы ранее использовали FlashPunk, вы будете знать, что type — это свойство, используемое в Entities для обнаружения столкновений. Обычно, когда вы проверяете наличие столкновений, вы проверяете тип, таким образом вы можете фильтровать столкновения (например, если вы хотите проверять столкновение с врагами, чтобы восстановить здоровье, вы будете проверять столкновения только с типом врага, поэтому вы не выполнять ненужные задачи, проверяя также столкновение со стенами).

В нашем случае тип будет уникальным для textInputs и будет использоваться для проверки столкновения. В мире FlashPunk есть метод проверки, существует ли сущность типа под точкой. Мы проверим, есть ли какой-либо textInput под точкой мыши. Если нет, мы установим фокус на ноль.

Сначала мы устанавливаем тип в конструкторе:

01
02
03
04
05
06
07
08
09
10
11
12
public function TextInput(x:Number=0, y:Number=0, text:String = "", multiline:Boolean = false)
{
    super(x, y);
     
    this.multiline = multiline;
     
    textGraphic = new Text("", 0, 0, { size: 16, color: 0xFFFFFF } );
    this.text = text;
    graphic = textGraphic;
     
    type = "uiTextInput";
}

Затем мы расширяем проверку нажатой мышью и добавляем условие else при столкновении с проверкой мышью, поэтому мы проверяем, нажимал ли пользователь какой-либо текстовый ввод при щелчке мыши:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
override public function update():void
{
    super.update();
     
    if (Input.mousePressed)
    {
        if (collidePoint(x, y, world.mouseX, world.mouseY))
        {
            TextInput.focus = this;
            Input.keyString = "";
        }
        else if (!world.collidePoint("uiTextInput", world.mouseX, world.mouseY)) TextInput.focus = null;
    }
     
    if (TextInput.focus == this && Input.keyString != "")
    {
        text += Input.keyString;
        Input.keyString = "";
    }
}

Вот и все.Теперь мы добавим дополнительную функцию: мы изменим цвет текста, если он сфокусирован. Он будет светло-синим, если сфокусирован, и полностью белым, если нет. Для этого нам просто нужно добавить эти две строки где-то в функции обновления:

1
2
3
4
5
6
7
8
9
override public function update():void
{
    super.update();
     
    if (TextInput.focus == this) textGraphic.color = 0xC4DCF4;
    else textGraphic.color = 0xFFFFFF;
     
    //[…]
}

И вот как это будет выглядеть, более или менее:


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

Вот код Используемые изображения доступны при загрузке исходного кода, а также сам код. По сути, он показывает изображение в зависимости от выбранного типа, затем скрывает или показывает прозрачные изображения в зависимости от выбранных функций:

001
002
003
004
005
006
007
008
009
010
011
012
013
014
+015
016
+017
018
019
020
021
022
023
024
025
026
027
028
029
+030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
+055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
package
{
    import net.flashpunk.FP;
    import net.flashpunk.graphics.Graphiclist;
    import net.flashpunk.graphics.Image;
    import net.flashpunk.graphics.Stamp;
    import net.flashpunk.graphics.Text;
    import net.flashpunk.World;
    import ui.Button;
    import ui.Checkbox;
    import ui.RadioButton;
    import ui.RadioButtonGroup;
    import ui.TextInput;
     
    public class PetCreator extends World
    {
        private var nameInput:TextInput;
         
        private var fishRad:RadioButton;
        private var octopusRad:RadioButton;
        private var snakeRad:RadioButton;
         
        private var hatCheck:Checkbox;
        private var wingsCheck:Checkbox;
        private var laserCheck:Checkbox;
        private var friendCheck:Checkbox;
         
        [Embed(source = "../assets/gfx/fish.png")] public static const FISH:Class;
        [Embed(source = "../assets/gfx/friend.png")] public static const FRIEND:Class;
        [Embed(source = "../assets/gfx/hat.png")] public static const HAT:Class;
        [Embed(source = "../assets/gfx/laser.png")] public static const LASER:Class;
        [Embed(source = "../assets/gfx/octopus.png")] public static const OCTOPUS:Class;
        [Embed(source = "../assets/gfx/snake.png")] public static const SNAKE:Class;
        [Embed(source = "../assets/gfx/wing.png")] public static const WING:Class;
         
        private var fish:Stamp;
        private var octopus:Stamp;
        private var snake:Stamp;
         
        private var friend:Stamp;
        private var hat:Stamp;
        private var laser:Stamp;
        private var wing:Stamp;
         
        public function PetCreator()
        {
            addGraphic(new Text("Pet Creator", 10, 10, { size: 32, color: 0xFFFFFF } ));
             
            //=============
             
            fish = new Stamp(FISH, 250, 20);
            octopus = new Stamp(OCTOPUS, 250, 20);
            snake = new Stamp(SNAKE, 250, 20);
             
            friend = new Stamp(FRIEND, 250, 20);
            hat = new Stamp(HAT, 250, 20);
            laser = new Stamp(LASER, 250, 20);
            wing = new Stamp(WING, 250, 20);
             
            octopus.visible = snake.visible = false;
            friend.visible = hat.visible = laser.visible = wing.visible = false;
             
            addGraphic(new Graphiclist(fish, octopus, snake, friend, hat, laser, wing));
             
            //===============
             
            addGraphic(new Text("Name:", 10, 80, { size: 16, color: 0x8CD5FB } ));
            add(nameInput = new TextInput(80, 80, "Pikachu"));
             
            addGraphic(new Text("Type:", 10, 120, { size: 16, color: 0x8CD5FB } ));
             
            var typeGroup:RadioButtonGroup = new RadioButtonGroup(onType);
            add(fishRad = new RadioButton(10, 150, typeGroup, "Fish", fish, true));
            add(octopusRad = new RadioButton(100, 150, typeGroup, "Octopus", octopus));
            add(snakeRad = new RadioButton(10, 200, typeGroup, "Snake", snake));
             
            addGraphic(new Text("Features:", 10, 270, { size: 16, color: 0x8CD5FB } ));
            add(hatCheck = new Checkbox(10, 300, "Hat", onFeature, hat));
            add(wingsCheck = new Checkbox(120, 300, "Wings", onFeature, wing));
            add(laserCheck = new Checkbox(10, 340, "Laser", onFeature, laser));
            add(friendCheck = new Checkbox(120, 340, "Friend", onFeature, friend));
             
            add(new Button(320, 290, "Random", onRandom));
        }
         
        private function onType(params:Object):void
        {
            fish.visible = octopus.visible = snake.visible = false;
            params.visible = true;
        }
         
        private function onFeature(on:Boolean, params:Object):void
        {
            params.visible = on;
        }
         
        private function onRandom():void
        {
            fish.visible = octopus.visible = snake.visible = false;
            fishRad.checked = octopusRad.checked = snakeRad.checked = false;
             
            var type:int = Math.random() * 3;
            if (type < 1)
            {
                fish.visible = true;
                fishRad.checked = true;
            }
            else if (type < 2)
            {
                octopus.visible = true;
                octopusRad.checked = true;
            }
            else if (type < 3)
            {
                snake.visible = true;
                snakeRad.checked = true;
            }
             
            hatCheck.checked = hat.visible = Math.random() < 0.5;
            wingsCheck.checked = wing.visible = Math.random() < 0.5;
            laserCheck.checked = laser.visible = Math.random() < 0.5;
            friendCheck.checked = friend.visible = Math.random() < 0.5;
             
            nameInput.text = FP.choose("Pikachu", "Link", "Slim Shady", "Abel Toy", "Me", "Meat Pet", "Daddy",
            "Leonardo DiCaprio", "Mr. Burns", "Peter", "TYPE YOUR NAME", "Derp");
        }
    }
}

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

Мы также немного расширим TextInput с действительно необходимым фоном и некоторым мигающим текстовым индикатором, таким как реальные текстовые поля. Мы также узнаем, как создать продвинутый компонент Slider, который представляет собой особую кнопку. Используя этот слайдер, мы также узнаем, как сделать полосу прокрутки.

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

Я надеюсь, что вы узнали, как создавать свои собственные компоненты пользовательского интерфейса, и что вам понравился учебник! Оставьте мне любые отзывы, предложения и так далее в комментариях ниже. Благодарность!