Портирование игры для INSTEAD во flash-приложение

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

Для начала несколько слов о движке INSTEAD. INSTEAD — движок для написания текстовых квестов (хотя есть и графические игры типа lines, пасьянс). Основная часть кода написана на lua, что позволяет достаточно просто портировать его на различные платформы.

Портирование обучающего примера

Рассмотрим структуру нашего проекта
Репозиторий
[DIR]InsteadFlash
 [DIR]adds
  [DIR]stead // в этой директории содержатся файлы lua-части движка INSTEAD
   ...
   flash.lua
   ...
   gui_flash.lua
   ...
  [DIR]theme
 [DIR]code
  Fish.xml
  PreloaderDum.as
  [DIR]neoart
   [DIR]flod // файлы проект FLOD (реализация классов для проигрывания mod файлов во flash)
    ...
    ModProcessor.as // модифицированный файл (метод play возвращает объект SoundTransform, добавлен сеттер soundTransform)
    ...
  [DIR]com
   [DIR]fish
    AMainClass.as
    i_Button.as
    [DIR]instead // порт кода INSTEAD на AS3
     FS.as
     Game.as
     Instead.as
     InsteadArgs.as
     InsteadCursor.as
     InsteadEvent.as
     InsteadImage.as
     InsteadLink.as
     InsteadPicture.as
     InsteadScroll.as
     InsteadText.as
     Main.as
     SaveSlot.as
    [DIR]screens // информационные экраны
     About.as
     Pause.as
  [DIR]games
   [DIR]tutorial3 // код портируемой INSTEAD игры (оригинальные lua-файлы)
    main-en.lua
    main-es.lua
    main-it.lua
    main-ru.lua
    main-ua.lua
    main.lua
  [DIR]mmg
   MouseWheel.as // класс для захвата мыши в AS3 проектах (см. Проблемма №3)
   SoundManager.as // модифицированный класс для проигрывания музыки и звуков Олега Антипова (добавлена поддержка mod файлов) 

При портировании игры нас интересует папка games и файлы SoundManager.as, InsteadPicture.as и FS.as (иногда задействуются файлы InsteadImage.as и InsteadLink.as).
В папке games будут находится файлы нашей игры, включая картинки и звуки.
В файле SoundManager.as мы интегрируем звуки и музыку игры во flash-приложение.
В файле InsteadPicture.as мы интегрируем изображения игры во flash-приложение.
В файле FS.as мы загрузим необходимые stead-модули и файлы игры.
В файлах InsteadImage.as и InsteadLink.as указываются графические файлы, которые будут отображаться в текстовой части игрового экрана.

Теперь обо всем по порядку.

Интегрирование музыки и звуков (исходный код файла SoundManager.as приведен не полностью)

package mmg
{

        import flash.media.Sound;
        import flash.media.SoundChannel;
        import flash.media.SoundTransform;
        import flash.events.Event;
        import flash.events.TimerEvent;
        import flash.utils.Timer;
        import flash.utils.ByteArray;
        import neoart.flod.*;

        dynamic public class SoundManager extends Object
        {

                [Embed(source='../../adds/theme/click.mp3')]
                public static const click:Class;

                [Embed(source='../games/tutorial3/ramparts.mp3')]
                public static const ramparts_mp3:Class;
                [Embed(source='../games/tutorial3/ramparts.mod', mimeType = "application/octet-stream")]
                public static const ramparts_mod:Class;
                private static var rampartsProcessor:ModProcessor = new ModProcessor();
                private static var mus:Object;  // хэш с музыкой
                static public var vol:Number=0.9; // громкость
                static public var volFadeSpeed:Number=0.05; //Скорость фейдинга
                static public var musEnable:Boolean=true; // включёна ли музыка
 
                static public var curMusName:String=""; // Имя текущего трека
                //внутренние переменные
                private static var curCh:SoundChannel; //Текущий канал
                private static var offCh:SoundChannel; //Затухающий канал
                private static var curVol:Number=0;
                private static var curPos:Number=0;
                private static var offVol:Number=0;
 
                private static var isplayed:Boolean=false;
 
                private static var timerVolFader:Timer= new Timer(10);
 
                private static var t1:SoundTransform = new SoundTransform();
                private static var t2:SoundTransform = new SoundTransform();
 
                static public function init():void 
                {
                        timerVolFader.stop();
                        // создадим объект с музыкой
                        mus = new Object();
                        //в качестве ключа объекта mus (в квадратных скобках) указываем путь относительно файла main.lua
                        //имя файла оставляем оригинальным, даже если файл был сконвертирован в другой формат
                        //Здесть втыкаем мод треки (если используем mod файл)
                        rampartsProcessor.load(new ramparts_mod as ByteArray);
                        mus["ramparts.mod"] = rampartsProcessor; // оставить нужно один из вариантов
                        //Здесь втыкаем свои муз треки (если сконвертировали mod в mp3)
                        mus["ramparts.mod"] = new ramparts_mp3(); // оставить нужно один из вариантов (этот я закомментировал при сборке)
                        
                        mus["click"] = new click();
                }
                ...
                часть исходного кода класса скрыта
                ...
        }
}

Все звуковые файлы хранятся в объекте mus[] = {}. Встраиваются файлы как обычно. Пример приведен как для mp3 файлов, так и для mod.
Добавление происходит в методе init().
Соответственно для добавления своих треков нужно встроить их во flash-приложение с помощью Embed и добавить в объект mus.

Интегрирование графических файлов (исходный код классов InsteadPicture.as, InsteadImage.as и InsteadLink.as приведен частично)

package com.fish.instead
{
        import flash.display.Sprite;
        import flash.display.Bitmap;
        import flash.events.Event;

        public class InsteadPicture extends Sprite
        {
                // graphic files
                [Embed(source='../../../games/tutorial3/instead.png')]
                public static const instead:Class;
                [Embed(source='../../../games/tutorial3/ru.png')]
                public static const ru:Class;
                [Embed(source='../../../games/tutorial3/ua.png')]
                public static const ua:Class;
                [Embed(source='../../../games/tutorial3/es.png')]
                public static const es:Class;
                [Embed(source='../../../games/tutorial3/gb.png')]
                public static const gb:Class;
                [Embed(source='../../../games/tutorial3/it.png')]
                public static const it:Class;
                // graphic files

                public static var gameFiles:Object = new Object();
                public var containerMid:Number = 0;
                private var hiding:Array = new Array;
                private var showing:Array = new Array;
                public var fading:Number = 0;

                public function InsteadPicture()
                {
                        gameFiles["instead.png"] = new instead;
                        gameFiles["ru.png"] = new ru;
                        gameFiles["ua.png"] = new ua;
                        gameFiles["es.png"] = new es;
                        gameFiles["gb.png"] = new gb;
                        gameFiles["it.png"] = new it;

                        for each (var value:Bitmap in gameFiles) 
                        {
                        // iterates through each value
                                addChild(value);
                                value.visible = false;
                                value.alpha = 0;
                        }
                        this.height = 0;
                        addEventListener(Event.ENTER_FRAME, DispatchPictures);
                }
                ...
                часть исходного кода класса скрыта
                ...
        }
}

Встраивание файлов производится стандартно, изображения хранятся в объекте gameFiles. Как ключ объекта gameFiles используется строка, с указанием пути к файлу относительно файла main.lua игры.
Если вместе вместе с текстом должны отображаться картинки, они должны быть прописаны в файле InsteadImage.as и InsteadLink.as.
Код InsteadImage.as

                ...
                public function InsteadImage(imgStr:String)
                {
                        gameFiles["ru.png"] = new InsteadPicture.ru;
                        gameFiles["ua.png"] = new InsteadPicture.ua;
                        gameFiles["es.png"] = new InsteadPicture.es;
                        gameFiles["gb.png"] = new InsteadPicture.gb;
                        gameFiles["it.png"] = new InsteadPicture.it;
                ...

Код InsteadLink.as

                ...
                public function InsteadLink(linkStr:String, isImage:Boolean = false, parent:Sprite = null)
                {
                        gameFiles["ru.png"] = new InsteadPicture.ru;
                        gameFiles["ua.png"] = new InsteadPicture.ua;
                        gameFiles["es.png"] = new InsteadPicture.es;
                        gameFiles["gb.png"] = new InsteadPicture.gb;
                        gameFiles["it.png"] = new InsteadPicture.it;
                ...


Как видно мы просто создаем экземпляры изображений в конструкторах InsteadImage и InsteadLink и помещаем их в объект gameFiles.

Интегрирование игровых файлов и модулей stead (исходный код файла FS.as)

package com.fish.instead
{
        import flash.utils.ByteArray;
        import flash.display.Bitmap;

        public class FS
        {
                [Embed(source='../../../../adds/stead/stead.lua', mimeType="application/octet-stream")]
                public static const stead_lua:Class;
                [Embed(source='../../../../adds/stead/gui.lua', mimeType="application/octet-stream")]
                public static const gui_lua:Class;
                [Embed(source='../../../../adds/stead/flash.lua', mimeType="application/octet-stream")]
                public static const flash_lua:Class;
                [Embed(source='../../../../adds/stead/goto.lua', mimeType="application/octet-stream")]
                public static const goto_lua:Class;
                [Embed(source='../../../../adds/stead/format.lua', mimeType="application/octet-stream")]
                public static const format_lua:Class;
                [Embed(source='../../../../adds/stead/vars.lua', mimeType="application/octet-stream")]
                public static const vars_lua:Class;
                [Embed(source='../../../../adds/stead/object.lua', mimeType="application/octet-stream")]
                public static const object_lua:Class;
                [Embed(source='../../../../adds/stead/dash.lua', mimeType="application/octet-stream")]
                public static const dash_lua:Class;
                [Embed(source='../../../../adds/stead/para.lua', mimeType="application/octet-stream")]
                public static const para_lua:Class;
                [Embed(source='../../../../adds/stead/quotes.lua', mimeType="application/octet-stream")]
                public static const quotes_lua:Class;
                [Embed(source='../../../../adds/stead/xact.lua', mimeType="application/octet-stream")]
                public static const xact_lua:Class;
                [Embed(source='../../../../adds/stead/timer.lua', mimeType="application/octet-stream")]
                public static const timer_lua:Class;
                // lua game files
                [Embed(source='../../../games/tutorial3/main.lua', mimeType="application/octet-stream")]
                public static const tutorial_main_lua:Class;
                [Embed(source='../../../games/tutorial3/main-en.lua', mimeType="application/octet-stream")]
                public static const tutorial_main_en_lua:Class;
                [Embed(source='../../../games/tutorial3/main-es.lua', mimeType="application/octet-stream")]
                public static const tutorial_main_es_lua:Class;
                [Embed(source='../../../games/tutorial3/main-it.lua', mimeType="application/octet-stream")]
                public static const tutorial_main_it_lua:Class;
                [Embed(source='../../../games/tutorial3/main-ru.lua', mimeType="application/octet-stream")]
                public static const tutorial_main_ru_lua:Class;
                [Embed(source='../../../games/tutorial3/main-ua.lua', mimeType="application/octet-stream")]
                public static const tutorial_main_ua_lua:Class;

                public static var gameFiles:Object = new Object();

                private static var _filesystemRoot:String;

                public static function filesystemRoot():String
                {
                        return _filesystemRoot;
                }

                public static function Init(libInitializer:*):void
                {
                        // game files
                        libInitializer.supplyFile("builtin://main.lua", new tutorial_main_lua() as ByteArray);
                        libInitializer.supplyFile("builtin://main-en.lua", new tutorial_main_en_lua() as ByteArray);
                        libInitializer.supplyFile("builtin://main-es.lua", new tutorial_main_es_lua() as ByteArray);
                        libInitializer.supplyFile("builtin://main-it.lua", new tutorial_main_it_lua() as ByteArray);
                        libInitializer.supplyFile("builtin://main-ru.lua", new tutorial_main_ru_lua() as ByteArray);
                        libInitializer.supplyFile("builtin://main-ua.lua", new tutorial_main_ua_lua() as ByteArray);                  
                        // stead modules
                        libInitializer.supplyFile("builtin://goto.lua", new goto_lua() as ByteArray);
                        libInitializer.supplyFile("builtin://format.lua", new format_lua() as ByteArray);
                        libInitializer.supplyFile("builtin://vars.lua", new vars_lua() as ByteArray);
                        libInitializer.supplyFile("builtin://object.lua", new object_lua() as ByteArray);
                        libInitializer.supplyFile("builtin://dash.lua", new dash_lua() as ByteArray);
                        libInitializer.supplyFile("builtin://para.lua", new para_lua() as ByteArray);
                        libInitializer.supplyFile("builtin://quotes.lua", new quotes_lua() as ByteArray);
                        libInitializer.supplyFile("builtin://xact.lua", new xact_lua() as ByteArray);
                        libInitializer.supplyFile("builtin://timer.lua", new timer_lua() as ByteArray);
                        _filesystemRoot = "builtin://";
                }
        }
}

Встраивание файлов игры и модулей stead производится стандартно с помощью Embed. Передача их в виртуальную файловую систему производится в методе Init — libInitializer.supplyFile(«builtin://_путь_к_файлу_относительно_корневой_папки_игры_», new _имя_класса_встроенного_файла_() as ByteArray).

Проблемы, возникшие при портировании движка на flash-платформу

Проблема №1. Lua-alchemy и встраиваемые файлы
На момент написания я не нашел примера использования встроенных файлов в lua-alchemy, тем более на русском языке. Это я и постараюсь сейчас исправить.
Интерпретатор lua-alchemy может использовать файлы, переданные ему с помощью функции supplyFile статического объекта LuaAlchemy.libInit. Для этого создадим класс FS со статическим методом Init:


package
{
    import flash.utils.ByteArray;
    import flash.display.Bitmap;

    public class FS
    {
        [Embed(source='file.lua', mimeType="application/octet-stream")]
        public static const file_lua:Class;

        private static var _filesystemRoot:String;

        public static function filesystemRoot():String
        {
            return _filesystemRoot;
        }

        public static function Init(libInitializer:*):void
        {
            libInitializer.supplyFile("builtin://file.lua",
                          new file_lua() as ByteArray);
            _filesystemRoot = "builtin://";
        }
    }
}

Теперь остается только инициализировать интерпретатор:


FS.Init(LuaAlchemy.libInit);
lua = new LuaAlchemy(FS.filesystemRoot());

и мы можем вызывать file.lua из интерпретатора, например dofile('file.lua').

Проблема №2. Отсутствующие core-функции lua-интерпретатора в lua-alchemy
Изначально lua-alchemy не поддерживает вызов некоторых функций lua-интерпретатора из ActionScript.
Список добавленных функций:
  • public function luaPop(n:uint):void
  • public function luaGetTop():int
  • public function luaGetGlobal(name:String):void
  • public function luaGetField(index:int, name:String):void
  • public function luaRemove(index:int):void
  • public function luaPCall(nargs:int, nresults:int, errfunc:int):int
  • public function luaToBoolean(index:int):Boolean
  • public function luaToString(index:int):String
  • public function luaPushNil():void
  • public function luaPushNumber(value:int):void
  • public function luaPushBoolean(value:int):void
  • public function luaPushString(value:String):void
Также реализована функция загрузки модулей lua require().
Репозиторий lua-alchemy с поддержкой вышеназванных функций на github.

Проблема №3. Прокручивание всей страницы при использовании колесика мыши во Flash-приложении
Эта проблема думаю известна многим, но способ борьбы с ней я нашел не сразу.
Dennis Kolyako реализовал отличный класс для захвата событий колеса мыши в AS3 проектах путем встраивания AS2 movie в AS3 movie. Это лучшее решение на сегодня.

Всего в паблик было выложено две игры (ссылки на Newgrounds):
Возвращение квантового кота
Побег из туалета
Ссылки не для пиара, а для демонстрации работоспособности проекта.
В игре «Побег из туалета» также используется парсер HTML-like кода, который генерируется движком INSTEAD. Решение использовать собственный парсер связанно с тем, что у TextField ужасная поддержка тэга img.

Благодарности
Хотелось бы поблагодарить:
Автора движка INSTEAD Петра Косых
Автора класса MouseWheel Дениса Коляко
Авторов проекта lua-alchemy
Авторов проекта Flod
Автора класса SoundManager Олега Антипова

Комментарии (12)

0
Мы выбираем QSP!
0
Мне не нравится QSP'шный синтаксис. Луа мне намного приятней.
0
Хорошие авторы вообще практически не умеют программировать. Им это не надо, им историю придумывать и текст писать интереснее.
+1
ох бляимхо лучше на флеше писать
0
Ну цель на самом деле была не создание платформы для новых текстовых квестов. Оверхед всё таки очень большой. Цель проекта — перенести существующие игры во флеш, тем более что движком INSTEAD поддерживаются игры для платформ URQ и TGE.
0
По моему целью было попеарить ИНСТЕД, который уже во всех дырках затычка. Видно что и регался ты с одной единственной целью.
+1
Оффтоп: А скажите, есть ли какие-то популярные текстовые флеш-игры?
И если есть, то немогли бы дать ссылку?)
0
Текстовые игры намного менее популярны чем графические, тем не менее вот эта имеет около полу милиона проигрываний на NG.
Это правда не квест, а новелла, но тем не менее.
0
Спасибо)
Жаль, что я ингилиш не шарю)))

А то что такие игры не популярны — это жаль.
Хотя в декстопной игре «Космические рейнжеры» были классно оформлены некоторые мисии, именно в виде текстовых квестов.
+1
Первый текстовый квест, с которым я познакомился- был «Стань стальной крысой» Гарри Гаррисона еще в начале\середине 90х, публиковался как дополнение к какому-то роману… Быстрое гугление нашло его www.mi.ru/~tonic/steelrat.html — надо будет еще разок сыграть
0
один из моих любимейших автров, стальная крыса в свое время очень понравилась)
0
Вот тут есть её порт. Вдруг будет удобней играть.
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.