Игра изнутри.

Vylgin Vitaly edited this page Aug 11, 2013 · 11 revisions

Вторая попытка написать статью, в первый раз, после часа работы над ней, кошка прошла по клавиатуре и обновила страницу, что привело к уничтожению всей проделанной работы... Что поделать, буду чаще сохраняться и не пускать кошку близко к ноутбуку.

Графика

Я нашел в задании некое противоречие. Нужно было использовать графические ресурсы fla и ai, но я нашел только возможность использования swc библиотеки, но для ее создания требуется flex, что противоречит условию использования чистого actionscript. После длительных поисков решения данной проблемы, я решил, что буду использовать чистый actionscript в том числе и для создания графических элементов игры.

Главное окно программы

MainWindowButton

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

Игровое окно

GameWindowScreenshot

Это основное окно игры, в которой происходит вся магия, которую я сейчас расскрою. Оно состоит из:

Заголовка

HeaderWindow

Кнопки закрытия

ExitButton

Виджета, который отображает количество дней в игре (DaysInGameWidget).

DaysInGameWidget

Виджета, который отображает число редких минералов (RareMiniralWidget).

RareMiniralWidget

Виджета, который отображает выпавшие элементы (RollLineWidget).

RollLineWidget

Виджета, который является кнопкой "SPIN", запускающей игровой процесс (SpinWidget).

SpinWidget

Виджета, на котором отображается справочная информация или описание выигрыша/проигрыша (PrizeWidget.as).

PrizeWidget

Виджета, на котором отображается текущее число попыток и расположена кнопка добавления попыток (AttemptWidget.as).

AttemptWidget

Кнопка, которая отображает окно с таблицей призов.

PrizeTableButton

Диалоговое окно добавления попыток

AttemptDialog

Данное окно состоит из текста и трех кнопок "Да", "Нет" и "Закрыть". Оно вызывается, если нажать на кнопку "Добавить" в игровом окне. Если в диалоговом окне нажать"Да", то в игровом окне отобразится увеличенное число попыток.

Окно таблицы призов

PrizeTableDialog

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

Как расположены графические компоненты

Нужно было решить, как располагать элементы относительно друг друга, я стал искать layout manager, входящий в библиотеку actionscript, но поиски затянулись и найти не удалось. Попадалось несколько решений 1, 2, 3, 4 , но для них требовался flex, или дополнительные сторонние библиотеки. Таким образом я решил жестко привязывать элементы, задавая им координаты, ширину и высоту.

Как устроены графические компоненты

Для работы с графикой были подключены пакеты flash.display и flash.geom. Большинство графических элементов игры являются наследниками класса Sprite, это означает, что можно на один спрайт добавить несколько дочерних элементов.

Для отображения и обработки событий кнопок был использован класс SimpleButton. Для примера, рассмотрим кнопку "SPIN" игрового окна, ее исходный код находится в классе SpinWidget, кнопка объявлена полем данного класса, ее инициализация и отображение в окне находится в методе initSpinButton().

SpinWidget SpinWidgetOver SpinWidgetDown

private function initSpinButton():void {
	spinButton = new SimpleButton();
    spinButton.x = rec.x / 4;
    spinButton.y = rec.y / 4;

    spinButton.upState = paintSpinButton(0x00ff00);
    spinButton.overState = paintSpinButton(0x66ff00);
    spinButton.downState = paintSpinButton(0x7fff00);
    spinButton.hitTestState = spinButton.upState;

    spinButton.addEventListener(MouseEvent.CLICK, spinButtonClickHandler);

    addChild(spinButton);
}

Как видно, кнопка имеет 4 состояния поведения, в зависимости от того, что мы совершаем мышкой. Для каждого из них мы вызываем метод paintSpinButton(color:Number), он возвращает спрайт с графическим отображением кнопки, в зависимости какой цвет передали в качестве аргумента, полученный спрайт становится дочерним элементом класса SpinWidget. Код метода paintSpinButton():

private function paintSpinButton(color:Number):Sprite {
    var x = rec.x + rec.width - 80;
    var y = rec.y + rec.height -210;
    var w = 40;
    var h = 80;

    var button:Sprite = new Sprite();
    with (button.graphics) {
        lineStyle(1, 0x000000);
        beginFill(color);
        drawRect(x, y, w, h);
        endFill();
    }

    var textField:TextField = new TextField();
    textField.text = "S\nP\nI\nN";
    textField.x = x;
    textField.y = y;
    textField.width = w;
    textField.height = h;
    textField.mouseEnabled = false;
    textField.setTextFormat(getCenterTextFormat());
    textField.defaultTextFormat = getCenterTextFormat();

    button.addChild(textField);

    return button;
}

Как видно из метода, текст отображается с помощью класса TextField.

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

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

PrizeTableDialog

private function initPrizeBoxes():void {
    var x:int = rec.x / 4 + 45;
    var y:int = rec.y / 4 + 60;
    var w:int = 190;
    var h:int = 40;

    var i;
    var j;
    for (i = 0, j = y; i < 5; i++, j+=45) {
        paintPrizeBox(x, j, w, h, i, 1);
    }

    for (i = 0, j = y; i < 5; i++, j+=45) {
        paintPrizeBox(x + w + 5, j, w, h, i, 2);
    }
}

Два цикла, каждый выводит на экран вертикальную колонку, в каждой колонке пять элементов, каждый элемент выводится на экран с помощью функции paintPrizeBox(x:int, y:int, w:int, h:int, count:int, row:int)

Изображение выпавших элементов

В исходной версии игры используются изображения, я думал также сделать, самостоятельно нарисовать средствами actionscript картинки. Но подумав, сколько времени это займет, решил заменить их обычными буквами, но оставив возможным в будущем подключить изображения в качестве выпавших элементов. За выпавшие символы отвечает общий интерфейс Symbol одноименного пакета, он содержит всего два метода получения массива символов и получения пустого символа, которые показываются при нажатии на кнопку "Go" главное окна.

package Symbol {
public interface Symbol {
    function getSymbols():Array;
    function getEmptySymbol():String;
}
}

Метод getEmptySymbol() возвращает значение типа String, но так как моих знаний языка actionscript пока не достаточно, чтобы подобрать наилучший способ как возвратить пустой символ абсолютно любого типа, как, например, указав класс Object языка java, то этот вопрос пока открыт. Если все-таки хочется использовать изображения, то можно возвращать пустой символ в виде пробела.

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

package Symbol {
public class AlphSimbol implements Symbol {
    private var gameWindow:GameWindow;
    private var symbol:Array;

    public function AlphSimbol(gameWindow:GameWindow) {
        this.gameWindow = gameWindow;

        symbol = new Array("A", "B", "C", "D", "E");
    }

    public function getSymbols():Array {
        return symbol;
    }

    public function getEmptySymbol():String {
        return "X";
    }
}
}

В классе игрового окна GameWindow есть поле private var symbol:Symbol; которому присваивается значение класса AlphSimbol.

    symbol = new AlphSimbol(this);

Получить символы можно путем вызова публичного метода класса GameWindow getSymbol

public function getSymbol():Symbol {
    return symbol;
} 

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

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

Игровая логика

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

Применение паттерна "Состояние"

На помощь пришла книга, которую я прочитал 2 года назад под назанием "Паттерны проектирования" издательства "Head First O'Reilly". В ней описывался паттерн проектирования под названием Состояние, его диаграмма классов выглядит следующим образом:

StateDiagram

Я решил, что данный паттерн именно то, что мне надо применить, для создания логики игры.

Логика игры состоит в том, чтобы при нажатии на кнопку "SPIN" начинался процесс смены состояний игрового процесса в определенном порядке. Для определения необходимых состояний я воспользовался интелект-картой (Mind Map) и набросал следующее:

StateMindMap

Сам процесс смены состояний можно отобразить коротко:

MovieState

Я создал пакет State, в котором реализовал следующую диаграмму классов:

StateDiagramGame

Каждый класс, реализующий интерфейс State, представляет собой определенное состояние игрового процесса.

Эти состояния добавлены в класс главного окна GameWindow как поля:

private var noPushSpinState:State;
private var pushSpinState:State;
private var startRotationState:State;
private var finishRotationState:State;
private var getPrizeState:State;

Также в класс GameWindow добавлено поле state:

private var state:State;

И методы setState и getState() для присваивания нового значение и получения ссылки на текущее состояние state:

public function setState(state:State):void {
    this.state = state;
}

public function getState():State {
    return state;
}

Для получения ссылки на необходимое состояние были добавлены следующие методы:

public function getNoPushSpinState():State {
    return noPushSpinState;
}

public function getPushSpinState():State {
    return pushSpinState;
}

public function getStartRotationState():State {
    return startRotationState;
}

public function getFinishRotationState():State {
    return finishRotationState;
}

public function getGetPrizeState():State {
    return getPrizeState;
}

Получить ссылку на текущее состояние игрового процесса можно так:

gameWindow.getState()

А установить текущее состояние игры в состояние NoPushSpinState можно так:

gameWindow.setState(gameWindow.getNoPushSpinState());  

Как видно, для создания игровой логики хватило 5 классов состояний:

  1. NoPushSpinState - кнопка SPIN не была нажата. В методе noPushSpin идет проверка, остались ли попытки, если да, то мы меняем состояние игрового процесса на PushSpinState, если нет, то делаем кнопку SPIN неактивной и ждем, пока пользователь пополнит общее число попыток.

     public function noPushSpin():void {
         if (gameWindow.getRollLineWidget().notRolls()) {
             gameWindow.getPrizeWidget().print("Нажмите на рычаг и испытайте\nсвою удачу!");
         }
    
         if (gameWindow.getAttemptWidget().getAttempt() > 0) {
             gameWindow.getSpinWidget().setSpinButtonEnabled(true);
             gameWindow.setState(gameWindow.getPushSpinState());
         } else {
             gameWindow.getSpinWidget().setSpinButtonEnabled(false);
             if (gameWindow.getRollLineWidget().notRolls()) {
                 gameWindow.getPrizeWidget().print("Закончились попытки!\nПополните их, нажав на кнопку \"Добавить\"");
             }
         }
     }
    

Это состояние присваивается полю state класса GameWindow в момент его создания. Оно также присваивается полю state после успешного выполнения метода getPrize состояния GetPrizeState.

  1. PushSpinState - возможность нажать на кнопку "SPIN". Это единственное состояние, которое не требует автоматического перехода в другое состояние, оно ожидает, когда будет нажата кнопка SPIN и когда это произошло, вызывается метод pushSpin(), который уменьшает число попыток на единицу, и состояние игры переходит в следущее состояние StartRotationState и запускает метод startRotation() нового состояния игрового процесса.

     public function pushSpin():void {
         var attemptWidget = gameWindow.getAttemptWidget();
         if (attemptWidget.getAttempt() > 0) {
             attemptWidget.setAttempt(attemptWidget.getAttempt() - 1);
         }
         gameWindow.setState(gameWindow.getStartRotationState());
         gameWindow.getState().startRotation();
     }
    
  2. StartRotationState - идет процесс отображения отображения, возможно, выигрышной комбинации изображений. Метод startRotation():

     public function startRotation():void {
         gameWindow.getPrizeWidget().print("Воспользуйтесь кнопкой \"Таблица призов\",\nчтобы узнать доступные комбинации\nи призы.");
         gameWindow.getRollLineWidget().roll();
     }
    

Вся магия появления изображений содержится в методе roll() класса RollLineWidget:

    public function roll():void {
        symbol = gameWindow.getSymbol().getSymbols();

        randomOne = randomNumber(5, 0);
        randomTwo = randomNumber(5, 0);
        randomThree = randomNumber(5, 0);

        oneTextField.text = " ";
        twoTextField.text = " ";
        threeTextField.text = " ";

        var timer:Timer = new Timer(1000, 4);
        timer.addEventListener(TimerEvent.TIMER, onTick);
        timer.addEventListener(TimerEvent.TIMER_COMPLETE, onTimerComplete);
        timer.start();
    }

Все оказывается очень просто, три целочисленных поля класса получают случайное значение от 0 до 4 таким образом:

    private function randomNumber(max:int, min:int = 0):int {
        return min + (max - min) * Math.random();
    }

И запускается таймер, который отчитывает 4 раза по 1 секунде. На него вешаются два слушателя. Один срабатывает каждый раз, когда происходит очередной тик таймера, вызывая метод onTick(). Раз в секунду отображается на экране очередной символ.

    private function onTick(event:TimerEvent):void {
        switch (event.target.currentCount) {
            case 1:
                oneTextField.text = symbol[randomOne];
                break;
            case 2:
                twoTextField.text = symbol[randomTwo];
                break;
            case 3:
                threeTextField.text = symbol[randomThree];
                break;
        }
    }

Второй срабатывает, когда таймер закончил свою работу, запускается метод onTimerComplete():

    private function onTimerComplete(event:TimerEvent):void {
        gameWindow.setState(gameWindow.getFinishRotationState());
        gameWindow.getState().finishRotation();
    }

У любопытного читателя может возникнуть вопрос, почему таймер отрабатывает 4 тика, если мы ловим только 3 первых из них? Это сделано для того, чтобы состояние игры изменилось и вызвался метод finishRotation(), после чего отобразится приз через 1 секунду, если он выпал, а не вовремя отображения третьего выпавшего символа.

  1. FinishRotationState - показ символов закончился. Это промежуточное состояние, которое служит для полноты картины, ведь если что-то началось, то должно и закончиться. Оно меняет состояние игры на состояние получения приза GetPrizeState и вызывает метод выдачи заслуженного вознаграждения, если, конечно, повезет.

     public function finishRotation():void {
         gameWindow.getPrizeWidget().print("Возможно Вы получили приз!");
         gameWindow.setState(gameWindow.getGetPrizeState());
         gameWindow.getState().getPrize();
     }
    
  2. GetPrizeState - Самое приятное для пользователя игровое состояние, если ему выпала удачная комбинация - он получат приз. Или самое печальное, если, бедняжке, не выпало ничего - приз его не ждет...

     public function getPrize():void {
         var text:String = gameWindow.getRollLineWidget().getPrize();
         if(gameWindow.getAttemptWidget().getAttempt() == 0) {
             gameWindow.getPrizeWidget().print(text + "\nЗакончились попытки!\nПополните их, нажав на кнопку \"Добавить\"");
         } else {
             gameWindow.getPrizeWidget().print(text);
         }
         gameWindow.setState(gameWindow.getNoPushSpinState());
         gameWindow.getState().noPushSpin();
     }
    

Информация о результате отображается на призовом виджете PrizeWidget в виде текста. Также выясняется, остались ли попытки, если нет, то к тексту прибавляется сообщение об этом. Информация о получении приза возвращается из метода getPrize() класса RollLineWidget:

    public function getPrize():String {
        var one = symbol[randomOne];
        var two = symbol[randomTwo];
        var three = symbol[randomThree];
        var textPrize:String = "Условия не сработали.";

        if (one != two && two != three && three != one) {
            textPrize = "Нет совпадений."
        } else if (one == two && two == three && three == one) {
            textPrize = getPrizeString(one, 3);
        } else if (one == two || two == three || three == one) {
            if (one == two) {
                textPrize = getPrizeString(one, 2);
            } else if (two == three) {
                textPrize = getPrizeString(two, 2);
            } else if (three == one) {
                textPrize = getPrizeString(three, 2);
            }
        }

        return textPrize;
    }

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

	Все выпавшие значения одинаковые.
	Все выпавшие значения разные.
	Первые два значения одинаковые, третье не имеет значения.
	Второе и третье значения одинаковые, первое не имеет значения.
	Третье и первое значения одинаковые, второе не имеет значения.	

После того, как вычислена, какая комбинация выпала, вызывается метод getPrizeString(sym:String, count:uint):String, в который в качестве аргументов передается одно из выпавших одинаковых значений и их количество. В зависимости от того, какие значения выпали, получаем итоговую строку и сам выигрыш:

    private function getPrizeString(sym:String, count:uint):String {
        var prize:String = "";

        if (sym == symbol[0]) {
            switch (count) {
                case 2:
                    gameWindow.getRareMiniralWidget().setRareMiniral(gameWindow.getRareMiniralWidget().getRareMiniral() + 10);
                    prize = sym + " " + sym + "\nВы выйграли 10 редких минералов";
                    break;
                case 3:
                    gameWindow.getRareMiniralWidget().setRareMiniral(gameWindow.getRareMiniralWidget().getRareMiniral() + 100);
                    prize = sym + " " + sym + " " + sym + "\nВы выйграли 100 редких минералов";
                    break;
            }
        } else if (sym == symbol[4]) {
            switch (count) {
                case 2:
                    gameWindow.getAttemptWidget().setAttempt(gameWindow.getAttemptWidget().getAttempt() + 2);
                    prize = sym + " " + sym + "\nВы выйграли 2 дополнительные попытки";
                    break;
                case 3:
                    gameWindow.getAttemptWidget().setAttempt(gameWindow.getAttemptWidget().getAttempt() + 5);
                    prize = sym + " " + sym + " " + sym + "\nВы выйграли 5 дополнительных попыток";
                    break;
            }
        } else {
            switch (count) {
                case 2:
                    gameWindow.getRareMiniralWidget().setRareMiniral(gameWindow.getRareMiniralWidget().getRareMiniral() + 1);
                    prize = sym + " " + sym + "\nВы выйграли 1 редкий минерал";
                    break;
                case 3:
                    gameWindow.getRareMiniralWidget().setRareMiniral(gameWindow.getRareMiniralWidget().getRareMiniral() + 3);
                    prize = sym + " " + sym + " " + sym + "\nВы выйграли 3 редких минерала";
                    break;
            }
        }

        return prize;
    }

Как только был получен, или не получен, приз и была выведена соответствующая информация на игровое окно, состояние игрового процесса переходит в уже знакомое состояние NoPushSpinState и вызывается метод noPushSpin(). И если пользователь снова нажмет на кнопку SPIN, если у него еще имеются попытки, то цикл переходов состояний повторится снова.

Заключение

Я доволен проделанной работой, приятно поиграть в собственноручно сделанную игру. Было интересно изучать язык actionscript, так как две с половиной недели назад у меня не было никакого опыта работы с ним, именно столько времени занял процесс написания. И кто знает, может я не в последний раз программирую на этом языке.

You can’t perform that action at this time.
You signed in with another tab or window. Reload to refresh your session. You signed out in another tab or window. Reload to refresh your session.
Press h to open a hovercard with more details.