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

В этом выпуске рассылки мы рассмотрим следующие вопросы, относящиеся к проектированию игр на Flash/Flex (Язык программирования ActionScript 3.0).


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

В этом выпуске рассылки мы рассмотрим следующие вопросы, относящиеся к проектированию игр на Flash/Flex (Язык программирования ActionScript 3.0).

  1. Загрузка и расстановка нескольких изображений
  2. Создание двухуровневого меню
  3. Использование ассоциативного массива.
  4. Использование отдельного класса для хранения общедоступных констант.

При этом везде, где это возможно, будем описывать необходимые величины, например, положение объектов и структуру меню, в файле формата XML.
Полный текст всех модулей проекта можно получить здесь. Для игры в начальном варианте нужны 4 изображения: для заднего плана, воздушного шарика, насоса и человечка. Имена соответствующих файлов перечислены ниже, в тексте файла XML, и, разумеется, могут быть другими. Сама программа пока не завершена и на сайте не публикуется..

Описание свойств объектов в формате XML

Рассмотрим начальный вариант файла описания. В дальнейшем добавим еще разделы.

<?xml version="1.0" encoding="utf-8"?>
<ball>
<pictures>
<pict name="storm.jpg" x="0" y="0" alpha="1" />
<pict name="pump.gif" x="120" y="250" alpha="1" />
<pict name="ball.gif" x="200" y="250" alpha=".6" />
<pict name="gena.gif" x="250" y="250" alpha="1" />
</pictures>
<menus>
<menu name="Установка" func="show">
<item name="Правила" func="temp" />
<item name="Выполнить" />
<item name="Сохранить" />
</menu>
<menu name="Игра" func="show">
<item name="Начать" />
<item name="Продолжить" />
<item name="Следующая сцена" />
<item name="Завершить" />
</menu>
</menus>
</ball>

В данном варианте текст состоит из двух разделов: pictures и menus. Поместим файлы с изображениями в папку images/forGames, и напишем имена каждого из этих файлов в атрибут name тега pict. В том же теге укажем и другие свойства изображения: координаты x и y, а также степень прозрачности alpha.

Расстановка картинок

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

private function set State(newState:String):void{
switch (newState){
case ("Start"):
_np =0;
var sp:Sprite;
while (sp = _pictures[ImgName]){ // Расставим все объекты в соответствии с XML-описанием
var cImage:XML = _rules..pictures.elements()[_np];
var xx:uint = int(cImage.@x); // Получение значений атрибутов
var yy:uint = int(cImage.@y);
var aa:Number = Number(cImage.@alpha); // Обратите внимание, как легко в данном языке преобразуется тип данных.
sp.x = xx;
sp.y = yy;
sp.alpha = aa;
addChild(sp);
_np++;
}
// Картинку заднего плана разместим отдельно, и назначим ей слушателя события.

_back.height = (_back.getChildAt(0) as Bitmap).height = Constants.HEIGHT1; // Картинку заднего плана установим отдельно
_back.width = (_back.getChildAt(0) as Bitmap).width = Constants.WIDTH1;
_back.x = _back.y = Constants.BORDER;
_back.addEventListener(MouseEvent.CLICK,menuHide); // Если пользователь щелкнет по картинке заднего плана, то подменю должно закрыться
addChild(_menu);
break;
}
_state = newState;
}

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

Преобразование типов данных (лирическое отступление)

Обратите внимание, как легко в языке ActionScript 3.0 преобразуется тип данных. var aa:Number = Number(cImage.@alpha);
Имя соответствующей функции совпадает с именем требуемого типа, а аргумент функции - данные любого типа. Очень изящное решение! Торжество принципа полиморфизма! Где-то на уровне реализации будет вызвана именно та функция преобразования, которая соответствует типу данных, передаваемых через аргумент. Правда, для преобразования к одному из типов данных: строковых существует специальная функция toString() - без аргумента. Но можно писать и по общему правилу: String(аргумент любого типа).
Чтобы лучше оценить это решение, вспомните, например, в Delphi есть функции StringTo
NB Среди типов данных в этом языке есть, кроме всех прочих Function. Аргумент этого типа имеется, в частности, в функциях определения слушателя события, например:
_back.addEventListener(MouseEvent.CLICK,menuHide);
Здесь menuHide - это имя функции, которая назначается слушателем события "щелчок мышкой". Заметьте, что это не строковая константа, иначе нужно было бы использовать кавычки.
Я подумал: а нельзя ли по аналогии с предыдущим примером, написать, например, так:
var func:Function = Function("myFunction"); //???
Если бы это получилось, то появилась бы возможность писать на этом языке еще красивее!
Эксперимент показал, что синтаксически здесь все правильно, формальных ошибок нет, но, увы, появляется ошибка выполнения:
EvalError: Error #1066: Функция формы ("function body") не поддерживается.
А жаль!

Построение меню по его XML-описанию

Раздел menus нашего XML-файла, текст которого приведен выше, состоит из элементов menu, каждый из которых соответствует пункту Верхнего меню. Например, тег: <menu name="Установка" func="show"> должен соответствовать позиции (кнопке) Главного меню, с нанесенной на ней надписи "Установка". Второй атрибут этого тега я добавил исключительно "для красоты", можно было бы обойтись без него, поскольку и так понятно, что если пункт относится к верхнему меню, то функция обработки события "щелчек мышкой" должна вызвать показ подменю. Правда, в некоторых случаях для этих кнопок подменю отсутствует.
Описание подменю состоит из тегов item, например: <item name="Выполнить" />. Наверное, понятно, что обработка этого тега должна привести к построению одной кнопки соответствующего подменю.
Ниже следует текст модуля Menu:

package
{
import flash.display.SimpleButton;
import flash.display.Sprite;
import flash.events.MouseEvent;
import flash.events.TimerEvent;
import flash.text.TextField;
import flash.text.TextFieldAutoSize;
import flash.utils.Timer;
public class Menu extends Sprite
{
private var __parent:ball;
private var _subMenus:Object = new Object;// Ассоциативный массив. Ключи - имена кнопок верхнего меню. Значения - указатели на подменю.
private var _timer:Timer = new Timer(2000);
public function Menu(pa:Object)
{
__parent = pa as ball;// связь с родительским объектом
_timer.addEventListener(TimerEvent.TIMER,tick);
}
public function getXml(xml:XML,xx:uint,yy:uint):void{

var xxx:uint = xx; // Аргументы функций не следует изменять внутри функций, поэтому пользуемся локальной переменной.

for each (var item:XML in xml..menu){
var bt:SimpleButton = btn(item,xxx,yy);
__parent.addChild(bt);
if (item.children().length()){ // Если у данного узла есть потомки
var yyy:uint = yy + bt.height+1;
var subMenu:Sprite = new Sprite; // то создать подменю
_subMenus[bt.name] = subMenu; // Записать указатель на объект в ассоциативный массив
for each (var subItem:XML in item.children()){
var sbt:SimpleButton = btn(subItem,xxx,yyy);
subMenu.addChild(sbt);
yyy += sbt.height + 1;
}

}
xxx += bt.width+5;
}
}
public function hide():void{
if (numChildren){ // если в данный момент подменю показано
_timer.stop(); // то остановить таймер
removeChildAt(0); // и скрыть подменю
}
}
private function btn(item:XML,xx:uint,yy:uint):SimpleButton{ // создание кнопки с надписью
var caption:String = item.@name;
var tx:TextField = txt(caption,xx,yy);
var tx1:TextField = txt(caption,xx,yy);
tx1.borderColor = 0xFF0000;
var tx2:TextField = txt(caption,xx,yy);
tx2.borderColor = 0x00FF00;
tx1.width = tx2.width = Constants.MENUWIDTH
var bt3:Sprite = rect(0xDDDDDD,xx,yy);
var bt:SimpleButton = new SimpleButton(tx,tx1,tx2,bt3);
bt.name = caption;
var func:String = item.@func;
switch (func){
case("show"): bt.addEventListener(MouseEvent.CLICK,show); break;
default: bt.addEventListener(MouseEvent.CLICK,btClick);
}
return bt;
}
private function show(e:MouseEvent):void{
var subMenu:Sprite = _subMenus[(e.target as SimpleButton).name];
hide();
addChild(subMenu);
_timer.start();
}
private function btClick(e:MouseEvent):void{

}
private function tick(e:TimerEvent):void{
if (numChildren){
removeChildAt(0);
}
_timer.stop();
}
private function txt(caption:String,xx:uint,yy:uint):TextField{
var tx:TextField = new TextField;
tx.x = xx+4;
tx.y = yy;
tx.height = Constants.MENUHEIGHT;
tx.width = Constants.MENUWIDTH;
tx.text = caption;
tx.border = true;
//tx.autoSize = TextFieldAutoSize.CENTER;
return tx;
}
private function rect(col:uint,xx:uint,yy:uint):Sprite{ // Рисует прямоугольник
var c_rect:Sprite = new Sprite();
with ( c_rect.graphics){
lineStyle(1,0);
beginFill(col);
drawRect(xx,yy,Constants.MENUWIDTH,Constants.MENUHEIGHT);
endFill();
}
return c_rect;
}
}
}

Построение меню выполняется в методе getXml(xml:XML,xx:uint,yy:uint). Просмотр документа XML происходит с помощью цикла: for each (var item:XML in xml..menu). Обратите внимание, что можно не знать, на каком уровне иерархии находится соответствующий раздел документа, достаточно знать его имя (имя соответствующего тега). Иначе говоря, тег <menu> может находиться внутри любого раздела документа, для его поиска достаточно написать две точки, как показано в этом примере.

Описания констант

Все константы в данном проекте я поместил в отдельный модуль, содержащий класс Constants. Вот его текст:
package
{
public class Constants
{
public static const PREFIX:String = "images/";
public static const PICTPREFIX:String = PREFIX + "forGames/";
public static const WIDTH:uint = 450
public static const HEIGHT:uint = 350;
public static const BORDER:uint = 20;
public static const WIDTH1:uint = WIDTH - BORDER*2
public static const HEIGHT1:uint = HEIGHT - BORDER*2;
public static const ALPHA:Number = Math.PI/16
// Константы для меню
public static const MENUHEIGHT:uint = BORDER - 4
public static const MENUWIDTH:uint = MENUHEIGHT * 4;
}
}

Сам по себе он особого интереса не представляет, и является всего лишь примером подобного использования классов. Кроме того, если бы я его не привел, Ваш проект бы не компилировался. Как видите, все константы объявлены с атрибутами public static. Первое понятно: зачем скрывать константу, раз ее все равно нельзя изменить? Использование атрибута static означает, что соответствующая информация находится в оперативной памяти постоянно, в течение всего времени работы программы. Это дает выигрыш во времени выборки, поскольку не требует обращения к диску, но несколько увеличивает требуемый объем оперативной памяти. Впрочем, при необходимости операционная система все равно отберет у приложения нужное количество разделов памяти, и отправит всю сохранявшуюся в ней информацию на диск, в файл подкачки.
Обратите внимание также, что значения некоторых констант определяются путем вычислений, а не простым присваиванием: еще одна приятная возможность ActionScript 3.0! При необходимости можно переопределить только основные константы, значения остальных изменятся автоматически. В нашем примере основными являются, например: WIDTH, HEIGHT и BORDER.

Ассоциативные массивы

Напомню, что в отличие от "обычного" массива, Ассоциативный состоит из двух компонентов: ключи и значения. Удобство их использования заключается в том, что нет необходимости помнить порядковый номер какого-либо элемента, достаточно знать его ключ. В качестве ключа можно использовать (почти) любое, обычно строковое значение. Этим облегчается поиск нужного элемента.
Для организации ассоциативного массива в ActionScript 3.0 используется экземпляр класса Object - суперкласса высшей категории, иначе говоря, класса, являющегося прародителем всех других в данной системе программирования. Для добавления элемента к массиву можно написать примерно так:
myArray[myKey] = myValue;
А для выборки значения по ключу:
needValue = myArray[myKey];
Есть и другие способы выполнить те же операции, но в данном проекте будем использовать эти приемы.

Меню на ActionScript 3.0

Рассмотрим текст модуля Menu:

package
{
import flash.display.SimpleButton;
import flash.display.Sprite;
import flash.events.MouseEvent;
import flash.events.TimerEvent;
import flash.text.TextField;
import flash.text.TextFieldAutoSize;
import flash.utils.Timer;
public class Menu extends Sprite
{
private var __parent:ball;
private var _subMenus:Object = new Object;// Ассоциативный массив. Ключи - имена кнопок верхнего меню. Значения - указатели на подменю.
private var _timer:Timer = new Timer(2000);
public function Menu(pa:Object)
{
__parent = pa as ball;
_timer.addEventListener(TimerEvent.TIMER,tick);
}
public function getXml(xml:XML,xx:uint,yy:uint):void{
var xxx:uint = xx; // Аргументы функций не следует изменять внутри функций, поэтому пользуемся локальной переменной.
for each (var item:XML in xml..menu){
var bt:SimpleButton = btn(item,xxx,yy);
__parent.addChild(bt);
if (item.children().length()){ // Если у данного узла есть потомки
var yyy:uint = yy + bt.height+1;
var subMenu:Sprite = new Sprite; // то создать подменю
_subMenus[bt.name] = subMenu; // Записать указатель на объект в ассоциативный массив
for each (var subItem:XML in item.children()){
var sbt:SimpleButton = btn(subItem,xxx,yyy);
subMenu.addChild(sbt);
yyy += sbt.height + 1;
}
}
xxx += bt.width+5; // Х-Координата следующей кнопки
}
}
public function hide():void{
if (numChildren){ // если в данный момент подменю показано
_timer.stop(); // то остановить таймер
removeChildAt(0); // и скрыть подменю
}
}
private function btn(item:XML,xx:uint,yy:uint):SimpleButton{ // создание кнопки с надписью
var caption:String = item.@name;
var tx:TextField = txt(caption,xx,yy);
var tx1:TextField = txt(caption,xx,yy);
tx1.borderColor = 0xFF0000;
var tx2:TextField = txt(caption,xx,yy);
tx2.borderColor = 0x00FF00; // при наведении указателя мыши цвет рамки меняется
tx1.width = tx2.width = Constants.MENUWIDTH
var bt3:Sprite = rect(0xDDDDDD,xx,yy);
var bt:SimpleButton = new SimpleButton(tx,tx1,tx2,bt3);
bt.name = caption; // Для записи имени объекта можно использовать кириллицу (благодаря исплользованию кодировки UTF-8
var func:String = item.@func;
switch (func){ //Назначим обработчик события. Список обработчиков может расширяться
case("show"): bt.addEventListener(MouseEvent.CLICK,show); break;
default: bt.addEventListener(MouseEvent.CLICK,btClick);
}
return bt;
}
private function show(e:MouseEvent):void{
var subMenu:Sprite = _subMenus[(e.target as SimpleButton).name]; // Обращение к ассоциативному массиву.
hide(); // Если одно из подменю уже показано, скрыть его.
addChild(subMenu);
_timer.start();
}
private function btClick(e:MouseEvent):void{ //Зарезервировано, в данном варианте пока не используется

}
private function tick(e:TimerEvent):void{ //Обработчик тика таймера
if (numChildren){
removeChildAt(0);
}
_timer.stop();
}
private function txt(caption:String,xx:uint,yy:uint):TextField{
var tx:TextField = new TextField;
tx.x = xx+4;
tx.y = yy;
tx.height = Constants.MENUHEIGHT;
tx.width = Constants.MENUWIDTH;
tx.text = caption;
tx.border = true;
return tx;
}
private function rect(col:uint,xx:uint,yy:uint):Sprite{ // Рисует прямоугольник
var c_rect:Sprite = new Sprite();
with ( c_rect.graphics){
lineStyle(1,0);
beginFill(col);
drawRect(xx,yy,Constants.MENUWIDTH,Constants.MENUHEIGHT);
endFill();
}
return c_rect;
}
}
}

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

  1. Найти указатель на нужное подменю (определяемое источником события).
  2. Скрыть подменю, имеющееся на экране в данный момент (если есть), удалив его из списка видимости
  3. Добавить найденное подменю к списку видимости.

Все это реализовано в фунции:
private function show(e:MouseEvent):void{
var subMenu:Sprite = _subMenus[(e.target as SimpleButton).name]; // Обращение к ассоциативному массиву.
hide(); // Если одно из подменю уже показано, скрыть его.
addChild(subMenu);
_timer.start();
}

Показанное подменю скрывается при наступлении одного из следующих событий:

  1. Истекло время демонстрации
  2. Пользователь выбрал другую позицию главного меню
  3. Пользователь щелкнул мышкой по изображению заднего плана.

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

Главный модуль

Текст главного модуля в его начальном состоянии приводится ниже с подробными комментариями:
package {
import flash.display.Bitmap;
import flash.display.Loader;
import flash.display.Sprite;
import flash.events.Event;
import flash.events.MouseEvent;
import flash.net.URLRequest;
public class ball extends Sprite
{
//*******************************
private var _loader:Loader = new Loader(); //Загрузчик. В данном случае - загрузчик изображений
private var _gena:Sprite= new Sprite(); // Объект для управления растровым изображением
private var _ball:Sprite= new Sprite(); // Объект для управления растровым изображением
private var _back:Sprite= new Sprite(); // Объект для хранения изображения заднего плана
private var _pump:Sprite= new Sprite();
private var _images:Array = new Array();
private var _sprites:Array = [_pump,_ball,_gena,_back];
private var _state:String;
private var _getKb:GetKbFromFile = new GetKbFromFile(this);
private var _rules:XML;
private var _np:uint;
private var _pictures:Object = new Object;
private var _menu:Menu = new Menu(this);
public function ball()
{
graphics.lineStyle(2,0); //Рисуем рабочее поле.
graphics.beginFill(0xFFFFFF);
with (Constants) graphics.drawRoundRect(0,0,WIDTH,HEIGHT,WIDTH / 10,HEIGHT / 10);
graphics.endFill();
_getKb.ask("ball.xml");
}
public function answer(what:String):void{
_rules = XML(what);
_menu = new Menu(this);
var menu:XML = XML(_rules..menus);
var str:String = menu.toXMLString();
_menu.getXml(menu,Constants.BORDER+5,2);
Np = 0; // Вызаваем сеттер, инициируем загрузку изображений.
}
private function set Np(newNp:uint):void{
_np = newNp;
if(_np <_rules..pictures.elements().length()){
bitmapLoad();
}else{
State = "Start";
}
}
private function bitmapLoad():void{ // загрузка файлов изображений, имена которых - в массиве _images
var imgName:String = Constants.PICTPREFIX+ImgName; // Обратимся к геттеру и получим полное имя вместе с URL
_loader.contentLoaderInfo.addEventListener(Event.COMPLETE,onComplete); //Связь со слушателем события "Завершение загрузки"
_loader.load(new URLRequest(imgName));
}

private function onComplete(e:Event):void{//Вызывается при завершении загрузки
var image:Bitmap = new Bitmap;
var sprite:Sprite = _sprites.pop(); // Выберем из массива объект, в который нужно поместить изображение
image = Bitmap(_loader.content); // Запишем результат загрузки в объект
sprite.addChild(image); // и свяжем его с управляемым объектом sprite.
sprite.width = image.width;
sprite.height = image.height;

_pictures[ImgName] = sprite; // Поместим информацию в ассоциативный массив
Np = _np + 1;// инициируем загрузку следующего изображения, вызвав Сеттер
}
private function set State(newState:String):void{
switch (newState){
case ("Start"):
_np =0;
var sp:Sprite;
while (sp = _pictures[ImgName]){ // Расставим все объекты в соответствии с XML-описанием
var cImage:XML = _rules..pictures.elements()[_np];
var xx:uint = int(cImage.@x);
var yy:uint = int(cImage.@y);
var aa:Number = Number(cImage.@alpha);
sp.x = xx;
sp.y = yy;
sp.alpha = aa;
addChild(sp);
_np++;
}
_back.height = (_back.getChildAt(0) as Bitmap).height = Constants.HEIGHT1; // Картинку заднего плана установим отдельно
_back.width = (_back.getChildAt(0) as Bitmap).width = Constants.WIDTH1;
_back.x = _back.y = Constants.BORDER;
_back.addEventListener(MouseEvent.CLICK,menuHide); // Если пользователь щелкнет по картинке заднего плана, то подменю должно закрыться
addChild(_menu);
break;
}
_state = newState;
}
private function menuHide(e:MouseEvent):void{
_menu.hide();
}
private function get ImgName():String{ //Возвращает имя картинки по _np
if(_np < _rules..pictures.elements().length()){
var cImage:XML = _rules..pictures.elements()[_np];
return cImage.@name;
}else{
return null;
}
}
}
}

Заключение

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


В избранное