Отправляет email-рассылки с помощью сервиса Sendsay
  Все выпуски  

Программирование игр на Flash/Flex Игра 'Ханойские башни' - полное решение


Добрый день, уважаемые читатели!

Рассмотрим ряд приемов программирования на языке ActionScript 3.0 и завершим разработку программы для реализации известной логической игры Ханойские башни.

Можно получить архив с полным текстом модулей проекта здесь.

Изучим следующие приемы программирования и разработки на языке ActionScript 3.0:

  • Рекурсивные методы;
  • Синхронизация процессов;
  • Использование интерфейса.

Рекурсивные методы

Во многих случаях для иллюстрации рекурсии преподаватели-программисты используют игру Ханойские башни. Именно в ней особенно ярко и наглядно видна сила этого универсального приема.
Напомню, что метод (функция) называется рекурсивным, если внутри его тела имеется один, или несколько обращений к этому методу, то есть, метод вызывает себя сам. Обращения (вызовы) подобного рода называются рекурсивными.
Простой пример рекурсивного метода: вычисление факториала числа. Как известно, факториал - это функция, определяемая следующим образом:

  • Факториал нуля равен единице;
  • Факториал числа N равен N умноженный на факториал от N-1.
или в математических обозначениях:
  • 0! = 1
  • N! = N*(N-1)!

Или на языке ActionScript 3.0:

public function factorial(n:uint):uint{
if(n == 0){
return 1;
}else{
return n * factorial(n - 1)
}
}

Вернемся к игре Ханойские башни. В соответствии с правилами этой игры, можно построить следующий рекурсивный алгоритм ее решения:
Пусть имеется три неподвижных диска, именуемые: ud1, ud2 и ud3. Чтобы перенести подвижный диск md с ud1 на ud3, используя в качестве промежуточного диск ud2, следует руководствоваться следующими правилами:

  • Если md - верхний диск на ud1, то перенести его на ud3 и завершить алгоритм;
  • Иначе:
    1. перенести диск mdu, лежащий на диске md, с диска ud1 на диск ud2, используя в качестве промежуточного диск ud3;
    2. перенести диск md на ud3;
    3. перенести диск mdu с диска ud2 на диск ud3, используя в качестве промежуточного диск ud1.

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

  1. Переносить диски по одному, предлагая пользователю каждый раз нажимать на кнопку, чтобы выполнить очередной перенос.
  2. Выполнять все переносы дисков последовательно, без участия пользователя.
    Реализация первого из этих методов демонстрируется, например, здесь. Событие, обработчик которого выполняет перенос диска, - это щелчек мышкой, выполняемый пользователем каждый раз, когда предыдущее аналогичное событие уже обработано программой. Пользователь в этом случае играет роль системы синхронизации, поскольку не выполнит очередной щелчек раньше, чем соответствующий диск переместится.
    Для реализации второго подхода нужно учитывать требования синхронизации процессов.

Синхронизация процессов

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

Текст функции перехода к начальному состоянию:

public function initState():void{
for (var i:uint = 0; i < Constants.NDISKS; i++){ // Удаление всех дисков из области видимости.
var dName:String = "d"+i.toString();
var disk:MovableDisk = findObjByName(dName) as MovableDisk;
if (this.getChildByName(dName)){
this.removeChild(disk);
}
}
_disk1.deleteAll();//Удаление всех подвижных дисков с неподвижных
_disk2.deleteAll();
_disk3.deleteAll();
for (i = 0; i < _nField.Value; i++){ //Установка нужного количества подвижных дисков на левый неподвижный.
dName = "d"+i.toString();
disk = findObjByName(dName) as MovableDisk;
addChild(disk);
_disk1.receiveDisk(disk);
}
}

Требуемое количество видимых подвижных дисков определяется текущим значением свойства _nField.Value.
Метод getChildByName(dName) - возвращает указатель на видимый объект по его имени, а метод findObjByName(dName) делает то же самое, но указатели хранятся не в области видимости рабочего поля, а в ассоциативном массиве Constants.Inds. Текст этого метода:

private function findObjByName(objName:String):Object{ //Возвращает указатель на объект по его имени.
return Constants.Inds[objName];
}

Непосредственно, переносом диска будут заниматься экземпляры класса MyTimer, потомка класса Timer. Обработчик события "тик таймера" в этом классе выглядит предельно просто:

private function tick(e:TimerEvent):void{
stop(); //Синхронизация процессов! Дождемся завершения этапа.
_md.changeParent(_d3,this,_state);
}

Как видите, останавливаем таймер и вызываем метод changeParent в соответствующем экземпляре класса MovableDisk. Метод changeParent также несложен: при каждом очередном вызове он перемещает диск сначала наверх, потом - до совпадения его центра с центром диска, на который нужно разместить, затем на верхнюю позицию пирамиды этого диска:

public function changeParent(newParent:UnmovableDisk,who:MyTimer,state:uint):void{
switch(state){
case(0):
_paDisk.deleteDisk();
who.step();//ждем следующего вызова (следующего тика таймера).
break;
case(1):
y = Constants.HIGH;
who.step();
break;
case(2):
Center = newParent.Center;
who.step();
break;
case(3):
newParent.receiveDisk(this);
who.ready();
break;
}
}

Текст функции step не нуждается в комментариях:

public function step():void{
_state++;
start();
}

Так, обмениваясь информацией, взаимодействуют между собой экземпляры двух классов. После завершения процесса переноса вызывается метод ready().
В данной программе этот метод имеет одинаковый смысл, и одинаковое имя в двух классах: Главном модуле hanoy и в модуле, описывающем класс MyTimer.

Использование интерфейса

Рассмотрим следующую задачу: в нескольких классах (в нашем случае - в двух) имеется метод, одинаковый по выполняемой задаче. Видимо, целесообразно присвоить ему одно и то же имя и снабдить одним и тем же набором аргументов.
В нашей задаче таким методом является ready(): когда агентам разных уровней нужно сообщить, что их задача выполнена, они вызывают данный метод в объекте - предке. На всех звеньях цепочки агентов, где участвуют экземпляры класса MyTimer, вызов метода приводит к продолжению переноса очередного диска, а на верхнем уровне, вызов метода ready() класса hanoy соответствует завершению процесса переноса всех дисков. Заметим, что объект - экземпляр класса должен по возможности быть одинаковым на любом уровне иерархии, поэтому, закончив свою задачу, он вызывает метод ready() своего предка, независимо от того, к какому классу принадлежит этот предок.

Для решения поставленной задачи следует применять интерфейс. В объектно-ориентированном программировании интерфейс олицетворяет собой набор операций, обеспечивающих определение видов услуг и способов их получения от программного объекта, предоставляющего эти услуги. Использование интерфейса в данном случае позволяет описывать создаваемый объект не ссылаясь на его класс: достаточно сослаться на соответствующий интерфейс. Поясним это примером.
Стандартный способ создания нового объекта - это строка кода примерно такого вида:
private var _disk1:UnmovableDisk = new UnmovableDisk(this);
Здесь мы ссылаемся на класс UnmovableDisk, поэтому при вызове любого публичного метода созданного объекта _disk1 будет вызываться соответствующий метод класса UnmovableDisk.
Если же имеется несколько классов, реализующих определенный набор методов с одинаковыми параметрами, то для создания объекта нужно писать примерно так:
private var _paTimer:Ihanoy;
Здесь Ihanoy - это ссылка на модуль с соответствующем именем, описывающего интерфейс. Текст этого модуля:
package
{
public interface Ihanoy
{
function ready():void;
}
}

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

Для того, чтобы связать класс с интерфейсом, то есть показать, что данный класс реализует методы, описанные в интерфейсе, в его описание добавляется ключевое слово implements, например:
public class hanoy extends Sprite implements Ihanoy
Теперь, полагаю несложно будет разобраться в том, как в нашей игре создается ссылка на объект. Это хорошо видно из описания конструктора класса MyTimer:
public function MyTimer(delay:Number,pa:Ihanoy)
{
super(delay);
addEventListener(TimerEvent.TIMER,tick);
_paTimer = pa;
}
Так мы добились, что при обращении к конструктору класса MyTimer ему передается указатель на родительский объект pa, являющегося экземпляром любого класса, реализующего интерфейс Ihanoy.
Свойство _paTimer описано так:
private var _paTimer:Ihanoy;

Заключение

По всем вопросам прошу обращаться по почте, или через любой Интернет-браузер. Мой адрес и другие контактные данные доступны зарегистрированным пользователям сайта. Желаю успехов!

В избранное