netlib.narod.ru< Назад | Оглавление | Далее >

Создание системы Mad Lib Script

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

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

Проектирование системы Mad Lib Script

Реализовать собственную систему MLS достаточно просто; создайте действия, которые хотите выполнять в своей игре с «белыми пятнами» (называемыми подставляемые элементы, entries), которые должны быть заполнены человеком создающим или редактирующим скрипт. Для каждого действия предоставьте список возможных значений для заполнения пустых элементов, которые могут быть разных типов — от строки текста до числа.

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

  1. Персонаж (*ИМЯ*) получает (*КОЛИЧЕСТВО*) повреждений

  2. Печатать (*ТЕКСТ*)

  3. Воспроизвести звуковой эффект (*ИМЯ_ЗВУКА*)

  4. Воспроизвести музыку (*ИМЯ_МУЗЫКИ*)

  5. Создать объект (*ИМЯ_ОБЪЕКТА*) в координатах (*XPOS*), (*YPOS*)

  6. Завершить обработку скрипта

У каждого из этих шести действий может быть произвольное количество пустых элементов, заключенных в скобки. Каждый из пустых элементов может хранить строку текста или число. Список действий и возможных значений элементов (с типом элемента) называется шаблоном действий (action template, пример вы можете увидеть на рис. 10.1).


Рис. 10.1. Шаблон действий разделяется на несколько действий, которые, в свою очередь, разделяются на элементы

Рис. 10.1. Шаблон действий разделяется на несколько действий, которые, в свою очередь, разделяются на элементы


Как только шаблон действий готов, вы можете ссылаться на действия по их номерам, а не по названиям (которые существуют только для того, чтобы пользователю было легче представить, какую функцию каждое из действий выполняет). Например, теперь я могу сказать, что хочу выполнить действие номер 4, используя title.mid в первом пустом элементе. Когда скрипт выполняется, система скриптов видит число 4 (действие номер 4) и знает, что оно требует только один элемент — имя файла с песней, который надо загрузить и воспроизвести.

ПРИМЕЧАНИЕ
Скриптовая система MLS будет работать в 90% ваших игр. Например, взгляните на игру RPG Maker (от Agetec, Inc) для игровой приставки Sony PlayStation. В ней вы можете создать свою собственную ролевую игру, работая с системой MLS-типа, похожей на ту, что я здесь описываю; поверьте мне, в этой игре вы можете создавать очень сложные скрипты.

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

Программирование системы Mad Lib Script

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

Чтобы писать скрипты было проще, используйте программу редактора скриптов (такую, как показана в разделе «Работа с редактором MLS» далее в этой главе) с шаблоном действий, что позволит вам быстро подбирать действия и изменять пустые элементы для них. Когда скрипт создан, вы можете прочитать файл скрипта из вашего движка и обработать каждое отдельное действие, используя указанные для каждого действия подставляемые элементы, которые были введены в редакторе скриптов.

Начнем мы с работы с шаблонами действий.

Работа с шаблонами действий

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

  1. Тип не задан.

  2. Текстовый элемент.

  3. Логическое значение.

  4. Целое число.

  5. Число с плавающей запятой.

  6. Выбор варианта (выбор из предопределенного списка текстовых значений).

У каждого типа подставляемого элемента есть собственные уникальные характеристики; строки могут быть переменного размера, числа могут находиться в диапазоне между двумя значениями, а логические значения могут быть TRUE или FALSE. Что же касается выбора варианта, у каждого варианта есть собственная текстовая строка (скрипт предлагает выбор из списка, а вместо текста используется индекс выбранного варианта).

Обычное действие может выглядеть так:

Действие #1: Произнесение заклинания для (*ВЫБОР_ВАРИАНТА*).
Возможные варианты для элемента #1:
1. Персонаж игрока
2. Произносящий заклинание
3. Цель закоинания
4. Никто

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

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

// Типы элементов (для пустых полей)
enum Types { _NONE = 0,
             _TEXT,
             _BOOL,
             _INT,
             _FLOAT,
             _CHOICE };

// Структура для хранения информации
// об одном пустом элементе
typedef struct sEntry {
    long Type; // Тип элемента (_TEXT, и т.д.)

    // Следующие два объединения хранят разную информацию
    // об отдельном подставляемом элементе, такую как
    // минимальные и максимальные значения (для целых чисел
    // и чисел с плавающей запятой) и номер варианта
    // для элемента с выбором вариантов. Текстовым и логическим
    // элементам эта информация не нужна.
    union {
        long  NumChoices; // Номер варианта в списке
        long  lMin;       // Минимальное значение для long
        float fMin;       // Минимальное значение для float
    };

    union {
        long  lMax;       // Максимальное значение для long
        float fMax;       // Максимальное значение для float
        char  **Choices;  // Массив текстовых описаний для вариантов
    };

    // Конструктор структуры, устанавливающий значения по умолчанию
    sEntry()
    {
        Type       = _NONE;
        NumChoices = 0;
        Choices    = NULL;
    }

    // Деструктор структуры, освобождающий используемые ресурсы
    ~sEntry()
    {
        // Особый случай для выбора варианта
        if(Type == _CHOICE && Choices != NULL) {
            if(NumChoices) {
                for(long i = 0; i < NumChoices; i++) {
                    delete [] Choices[i]; // Удаляем описание варианта
                    Choices[i] = NULL;
                }
            }
            delete [] Choices; // Удаляем массив описаний
            Choices = NULL;
        }
    }
} sEntry;

// Структура хранит отдельное действие и содержит указатель
// для связанного списка
typedef struct sAction {
    long    ID; // ID действия (от 0 до количества действий минус 1)

    char    Text[256];  // Название действия

    short   NumEntries; // Количество элементов в действии
    sEntry  *Entries;   // Массив структур элементов

    sAction *Next;      // Следующее действие в связанном списке

    sAction()
    {
        ID         = 0; // Устанавливаем значения по умолчанию
        Text[0]    = 0;
        NumEntries = 0;
        Entries    = NULL;
        Next       = NULL;
    }

    ~sAction()
    {
        delete [] Entries;
        Entries = NULL;    // Освобождаем массив элементов
        delete Next;
        Next = NULL;       // Удаляем следующий элемент
    }
} sAction;

Вы совместно используете две показанных структуры sEntry и sAction для хранения описания действия а также типа каждого подставляемого элемента. Типы элементов вы выбираете из предопределенного списка (как было описано ранее в этом разделе). Кроме того, структура sEntry содержит правила для каждого типа элемента (используя два объединения).

Поскольку текстовый элемент это только буфер символов, при использовании данного типа нет никаких правил, которым надо следовать. То же самое касается и логических значений, поскольку они могут быть только TRUE или FALSE. Для целых чисел и чисел с плавающей запятой нужны минимальное и максимальное значения для задания диапазона допустимых значений (отсюда переменные min/max). Для выбора варианта необходимо количество вариантов и массив символьных буферов с названиями для каждого варианта.

Структура sAction хранит идентификатор действия (номер действия в общем списке действий), текст действия и массив подставляемых элементов, используемых для этого действия. Чтобы определить количество подставляемых элементов в действии (а также тип каждого), необходимо проанализировать текст действия. Для вставки элемента в текст действия используется символ тильды (~), как показано ниже:

Игрок ~ получает ~ очков повреждений

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

Шаблон действий хранится в виде текстового файла, в котором текст каждого действия заключен в кавычки. За каждым действием, у которого есть подставляемые элементы (отмеченные в тексте тильдами) следует список параметров элементов. Каждое описание подставляемого элемента начинается со слова, определяющего тип элемента (TEXT, BOOL, INT, FLOAT или CHOICE). В зависимости от типа элемента, дальше может следовать дополнительная информация.

Для типа TEXT дополнительная информация не требуется. То же относится и к типу BOOL. Для INT и FLOAT необходимы минимальное и максимальное значения. И, наконец, за элементом CHOICE следует количество вариантов выбора и текст для каждого варианта (заключенный в кавычки).

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

"Печатать ~"
TEXT
"Переместить персонаж в ~, ~, ~"
FLOAT 0.0 2048.0
FLOAT 0.0 2048.0
FLOAT 0.0 2048.0
"Персонаж ~ ~ ~ очков ~"
CHOICE 3
"Главный персонаж"
"Заклинатель"
"Цель"
CHOICE 2
"Получает"
"Теряет"
INT 0 128
CHOICE 2
"Здоровья"
"Магии"
"Установить переменную ~ в ~"
INT 0 65535
BOOL
"Конец скрипта"

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

Загрузка шаблона действий — это вариация на тему обработки текстового файла с инициализацией соответствующих структур, состоящая из сравнений строк для загружаемых слов и сохранения расположенного внутри кавычек текста. Это действительно простой процесс и в разделе «Совмещаем все вместе в классе cActionTemplate» вы точно узнаете, как это сделать.

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

Создание элементов скрипта

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

typedef struct sScriptEntry
{
    long Type; // Тип элемента (_TEXT, _BOOL, и т.д.)

    union {
        long  IOValue;   // Используется для сохранения и загрузки
        long  Length;    // Длина текста (с завершающим 0)
        long  Selection; // Выбранный вариант
        BOOL  bValue;    // Логическое значение
        long  lValue;    // Целое значение
        float fValue;    // Значение с плавающей точкой
    };

    char *Text; // Текстовый буфер

    sScriptEntry()
    {
        Type    = _NONE; // Устанавливаем значения по умолчанию
        IOValue = 0;
        Text    = NULL;
    }

    ~sScriptEntry()
    {
        delete [] Text;
        Text = NULL;
    } // Удаляем текстовый буфер
} sScriptEntry;

Во многом похожая на sEntry, sScriptEntry хранит действительные значения, используемые в каждом пустом элементе действия. Здесь вы снова видите член Type. Он описывает тип элемента (_TEXT, _BOOL и т.д.). Единственное объединение является самой главной частью; в нем одна переменная для длины текста, одна для номера выбора и по одной для целочисленных значений, значений с плавающей точкой и логических значений.

Следует отметить два момента, относящихся к sScriptEntry. Во-первых, указатель на строку символов находится вне объединения (потому что для хранения текстовых данных используются и Text и Length). Во-вторых, в объединение включена дополнительная переменная IOValue. Вы используете IOValue для доступа к переменным объединения при сохранении и загрузке данных подставляемого элемента.

Чтобы продемонстрировать, как данные каждого подставляемого элемента хранятся в структуре sScriptEntry (или в структурах, если у вас несколько элементов), посмотрите на следующие действия:

"~ здоровье игрока на ~"
CHOICE 2
"Увеличить"
"Уменьшить"
INT 0 65535

В зависимости от выбранного в первом элементе варианта, показанное действие увеличивает или уменьшает значение здоровья игрока на заданную величину в диапазоне от 0 до 65 535. Поскольку здесь два подставляемых элемента (выбор варианта и целое число), вам понадобятся две структуры sScriptEntry:

sScriptEntry Entries;
// Конфигурирование выбора варианта,
// устанавливаем первый вариант
Entries[0].Type      = _CHOICE;
Entries[0].Selection = 0; // Увеличение

// Конфигурирование целочисленного значения,
// устанавливаем 128
Entries[1].Type   = _INT;
Entries[1].lValue = 128;

Когда мы имеем дело с элементами скрипта, возникает достаточно сложная задача, когда в завершенном скрипте много элементов. Каждому действию скрипта необходима соответствующая структура sEntry, которая, в свою очередь, может содержать несколько структур sScriptEntry. Прежде чем думая об этом вы окажетесь по колено в структурах, поговорим о беспорядке! Для лучшей обработки структур скрипта нам нужна другая структура, которая отслеживает каждый элемент, принадлежащий действию скрипта.

typedef struct sScript
{
    long Type;       // от 0 до (количество действий - 1)
    long NumEntries; // Количество элементов в данном действии скрипта

    sScriptEntry *Entries; // Массив подставляемых элементов

    sScript *Prev; // Предыдущий элемент связанного списка
    sScript *Next; // Следующий элемент связанного списка

    sScript()
    {
        Type = 0; // Установка значений по умолчанию
        NumEntries = 0;
        Entries = NULL;
        Prev = Next = NULL;
    }

    ~sScript()
    {
        delete [] Entries;
        Entries = NULL; // Удаление массива элементов

        delete Next; // Удаление следующего элемента
        Next = NULL; // связанного списка
    }
} sScript;

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

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

И, наконец, два указателя в sScript — Prev и Next. Эти два указателя управляют связанным списком, образующим скрипт. Для создания связанного списка структур sScript (такого, как показанный на рис. 10.2), начните с корневой структуры, представляющей первое действие скрипта. Затем свяжите структуры sScript через переменные Next и Prev, как показано ниже:

sScript *ScriptRoot = new sScript();
sScript *ScriptPtr  = new sScript();

ScriptRoot->Next = ScriptPtr; // Указатель на второе действие

ScriptPtr->Prev = ScriptRoot; // Указатель обратно на корень

Рис. 10.2. Связанный список действий скрипта

Рис. 10.2. Связанный список действий скрипта использует переменные Prev и Next для объединения скрипта в единое целое. У каждого действия скрипта есть собственный массив элементов


Теперь вы можете начать с корня скрипта и пройти вниз по всему скрипту с помощью следующего кода:

void TraverseScript(sScript *pScript)
{
    while(pScript != NULL) { // Цикл, пока в скрипте больше не останется действий

        // Делаем что-нибудь с pScript
        // pScript->Type хранит ID действия скрипта

        pScript = pScript->Next; // Переходим к следующему действию скрипта
    }
}

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

BOOL SaveScript(char *Filename, sScript *ScriptRoot)
{
    FILE *fp;
    long i, j, NumActions;
    char Text[256];
    sScript *ScriptPtr;

    // Убеждаемся, что в скрипте есть действия
    if((ScriptPtr = ScriptRoot) == NULL)
        return FALSE;

    // Подсчет количества действий
    NumActions = 0;
    while(ScriptPtr != NULL) {
        NumActions++;                // Увеличиваем счетчик
        ScriptPtr = ScriptPtr->Next; // Следующее действие
    }

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

    // Открываем выходной файл
    if((fp = fopen(Filename, "wb")) == NULL)
        return FALSE;         // Сообщаем об ошибке

    // Выводим количество действий в скрипте
    fwrite(&NumActions, 1, sizeof(long), fp);

    // Перебираем каждое действие скрипта
    ScriptPtr = ScriptRoot;
    for(i = 0; i < NumActions; i++) {
        // Выводим тип действия и количество элементов в нем
        fwrite(&ScriptPtr->Type, 1, sizeof(long), fp);
        fwrite(&ScriptPtr->NumEntries, 1, sizeof(long), fp);

        // Выводим данные подставляемых элементов (если они есть)
        if(ScriptPtr->NumEntries) {
            for(j = 0; j < ScriptPtr->NumEntries; j++) {
                // Записываем тип элемента и данные
                fwrite(&ScriptPtr->Entries[j].Type, 1, sizeof(long), fp);
                fwrite(&ScriptPtr->Entries[j].IOValue, 1, sizeof(long), fp);

                // Записываем текст элемента (если есть)
                if(ScriptPtr->Entries[j].Type == _TEXT &&
                                  ScriptPtr->Entries[j].Text != NULL)
                    fwrite(ScriptPtr->Entries[j].Text, 1,
                           ScriptPtr->Entries[j].Length, fp);
            }
        }
        // Переходим к следующей структуре в связанном списке
        ScriptPtr = ScriptPtr->Next;
    }
    fclose(fp);
    return TRUE; // Сообщаем об успехе!
}

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

sScript *LoadScript(char *Filename, long *NumActions)
{
    FILE *fp;
    long i, j, Num;
    char Text[2048];

    sScript *ScriptRoot, *Script, *ScriptPtr = NULL;

    // Открываем файл для ввода
    if((fp = fopen(Filename, "rb")) == NULL)
        return NULL;

    // Получаем количество действий в файле скрипта
    fread(&Num, 1, sizeof(long), fp);

    // Сохраняем количество действий в предоставленной
    // пользователем переменной
    if(NumActions != NULL) *NumActions = Num;

    // Цикл по каждому действию скрипта
    for(i = 0; i < Num; i++) {
        // Выделяем память для структуры скрипта и привязываем ее
        Script = new sScript();
        if(ScriptPtr == NULL)
            ScriptRoot = Script; // Назначаем корень
        else
            ScriptPtr->Next = Script;

        ScriptPtr = Script;

        // Получаем тип действия и количество элементов
        fread(&Script->Type, 1, sizeof(long), fp);
        fread(&Script->NumEntries, 1, sizeof(long), fp);

        // Получаем данные элементов (если они есть)
        if(Script->NumEntries) {
            // Выделяем память для массива элементов
            Script->Entries = new sScriptEntry[Script->NumEntries]();

            // Загружаем каждый элемент
            for(j = 0; j < Script->NumEntries; j++) {
                // Получаем тип элемента и данные
                fread(&Script->Entries[j].Type, 1, sizeof(long), fp);
                fread(&Script->Entries[j].IOValue, 1, sizeof(long), fp);

                // Получаем текст (если есть)
                if(Script->Entries[j].Type == _TEXT &&
                                     Script->Entries[j].Length) {
                    // Выделяем буфер и получаем строку
                    Script->Entries[j].Text =
                            new char[Script->Entries[j].Length];
                    fread(Script->Entries[j].Text, 1,
                          Script->Entries[j].Length, fp);
                }
            }
        }
    }
    fclose(fp);
    return ScriptRoot;
}

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

Функция LoadScript открывает файл скрипта и строит связанный список структур sScript на основании загруженных данных. Образующие связанный список структуры sScriptEntry и sScript создаются на лету. После завершения работы функция LoadScript инициализирует переменную NumActions, записывая в нее количество загруженных действий, и возвращает указатель на корневую структуру скрипта.

Объединяем все в классе cActionTemplate

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

class cActionTemplate
{
  private:
    long    m_NumActions;    // Количество действий в шаблоне
    sAction *m_ActionParent; // Список действий шаблона

    // Функции чтения текста (в основном используются в действиях)
    BOOL GetNextQuotedLine(char *Data, FILE *fp, long MaxSize);
    BOOL GetNextWord(char *Data, FILE *fp, long MaxSize);

  public:
    cActionTemplate();
    ~cActionTemplate();

    // Загрузка и освобождение шаблона действий
    BOOL Load(char *Filename);
    BOOL Free();

    // Получение количества действий в шаблоне,
    // родительского действия и заданной структуры действия
    long GetNumActions();
    sAction *GetActionParent();
    sAction *GetAction(long Num);

    // Получение структуры sScript заданного типа
    sScript *CreateScriptAction(long Type);

    // Получение информации о действиях и элементах
    long GetNumEntries(long ActionNum);
    sEntry *GetEntry(long ActionNum, long EntryNum);

    // Формирование текста действия с использованием
    // значений по умолчанию
    BOOL ExpandDefaultActionText(char *Buffer, sAction *Action);

    // Формирование текста действия с использованием
    // установленных значений
    BOOL ExpandActionText(char *Buffer, sScript *Script);
};

В коде есть две функции, с которыми мы еще не встречались в этой главе, — GetNextQuotedLine и GetNextWord. Функция GetNextQuotedLine сканирует файл, выбирая заключенную в кавычки строку текста, а функция GetNextWord читает из файла очередное слово. Обе функции получают указатель на буфер данных, в котором будут сохранять текст, указатель для доступа к файлу и максимальный размер буфера данных (для предотвращения переполнения).

BOOL cActionTemplate::GetNextQuotedLine(char *Data,
                                        FILE *fp, long MaxSize)
{
    int  c;
    long Pos = 0;

    // Читаем, пока не найдем кавычки (или EOF)
    while(1) {
        if((c = fgetc(fp)) == EOF)
            return FALSE;

        if(c == '"') {

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

            // Читаем, пока не достигнем следующей кавычки (или EOF)
            while(1) {
                if((c = fgetc(fp)) == EOF)
                    return FALSE;

                // Обнаружив вторую кавычку возвращаем текст
                if(c == '"') {
                    Data[Pos] = 0;
                    return TRUE;
                }

                // Добавляем допустимые символы к строке
                if(c != 0x0a && c != 0x0d) {
                    if(Pos < MaxSize - 1)
                        Data[Pos++] = c;
                }
            }
        }
    }
}

Функция GetNextWord работает почти так же, как GetNextQuotedLine — она последовательно считывает символы в буфер, пока не будет обнаружен конец строки или пробел (оба отмечают конец загружаемого слова).

BOOL cActionTemplate::GetNextWord(char *Data, FILE *fp,
                                  long MaxSize)
{
    int  c;
    long Pos = 0;

    // Очищаем строку
    Data[0] = 0;

    // Читаем, пока не найдем допустимый символ
    while(1) {
        if((c = fgetc(fp)) == EOF) {
            Data[0] = 0;
            return FALSE;
        }

        // Проверка на начало слова
        if(c != 32 && c != 0x0a && c != 0x0d) {
            Data[Pos++] = c;

            // Цикл, пока не достигнем конца слова (или EOF)
            while((c = fgetc(fp)) != EOF) {
                // Прерываемся на разделителе слов
                if(c == 32 || c == 0x0a || c == 0x0d)
                    break;

                // Добавляем символ, если есть место
                if(Pos < MaxSize - 1)
                    Data[Pos++] = c;
            }
            // Добавляем символ окончания строки
            Data[Pos] = 0;
            return TRUE;
        }
    }
}

Используя функции GetNextQuotedLine и GetNextWord вы можете просканировать содержащий описание действий текст входного файла, что и делает функция cActionTemplate::Load:

BOOL cActionTemplate::Load(char *Filename)
{
    FILE *fp;
    char Text[2048];

    sAction *Action, *ActionPtr = NULL;
    sEntry  *Entry;

    long i, j;

    // Освобождаем предыдущие структуры действий
    Free();

    // Открываем файл действий
    if((fp = fopen(Filename, "rb")) == NULL)
        return FALSE;

    // Цикл, пока не достигнем конца файла
     while(1) {

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

        // Получаем следующее действие в кавычках
        if(GetNextQuotedLine(Text, fp, 2048) == FALSE)
            break;

        // Выход, если нет текста действия
        if(!Text[0])
            break;

        // Создаем структуру действия и присоединяем
        // ее к списку
        Action = new sAction();
        Action->Next = NULL;

        if(ActionPtr == NULL)
            m_ActionParent = Action;
        else
            ActionPtr->Next = Action;

        ActionPtr = Action;

        // Копируем текст действия
        strcpy(Action->Text, Text);

        // Сохраняем ID действия
        Action->ID = m_NumActions;

        // Увеличиваем счетчик загруженных действий
        m_NumActions++;

        // Подсчитываем количество элементов в действии
        for(i = 0; i < (long)strlen(Text); i++) {
            if(Text[i] == '~')
                Action->NumEntries++;
        }

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

        // Выделяем память и читаем элементы (если есть)
        if(Action->NumEntries) {
            Action->Entries = new sEntry[Action->NumEntries]();
            for(i = 0; i < Action->NumEntries; i++) {
                Entry = &Action->Entries[i];

                // Получаем тип элемента
                GetNextWord(Text, fp, 2048);

                // Тип TEXT, нет последующих данных
                if(!stricmp(Text, "TEXT")) {

В показанном фрагменте кода вы проверяете совпадает ли первое слово в описании элемента со строкой «TEXT». Если да, значит тип данного элемента TEXT и вы должны получить текстовую строку в качестве данных элемента. Последующий код проверяет другие типы элементов (INT, BOOL и т.д.) и загружает последующие данные, согласно объявленному типу элемента:

                    // Устанавливаем текстовый тип
                    Entry->Type = _TEXT;
                } else // Если не TEXT, проверяем тип INT
                // Тип INT, получаем минимальное
                // и максимальное значение
                if(!stricmp(Text, "INT")) {
                    // Устанавливаем тип INT и создаем элемент INT
                    Entry->Type = _INT;

                    // Получаем минимальное значение
                    GetNextWord(Text, fp, 2048);
                    Entry->lMin = atol(Text);

                    // Получаем максимальное значение
                    GetNextWord(Text, fp, 2048);
                    Entry->lMax = atol(Text);
                } else // Если не INT, проверяем тип FLOAT
                // Тип FLOAT, получаем минимальное и
                // максимальное значения
                if(!stricmp(Text, "FLOAT")) {
                    // Устанавливаем тип FLOAT и создаем
                    // элемент FLOAT
                    Entry->Type = _FLOAT;

                    // Получаем минимальное значение
                    GetNextWord(Text, fp, 2048);
                    Entry->fMin = (float)atof(Text);

                    // Получаем максимальное значение
                    GetNextWord(Text, fp, 2048);
                    Entry->fMax = (float)atof(Text);
                } else // Если не FLOAT, проверяем тип BOOL
                // Тип BOOL, нет параметров
                if(!stricmp(Text, "BOOL")) {
                    // Устанавливаем тип BOOL и создаем элемент BOOL
                    Entry->Type = _BOOL;
                } else // Если не BOOL, проверяем тип CHOICE
                // Тип CHOICE, получаем количество вариантов
                // и их текст
                if(!stricmp(Text, "CHOICE")) {
                    // Устанавливаем тип CHOICE и создаем элемент CHOICE
                    Entry->Type = _CHOICE;

                    // Получаем количество вариантов
                    GetNextWord(Text, fp, 1024);
                    Entry->NumChoices = atol(Text);
                    Entry->Choices = new char[Entry->NumChoices];

                    // Получаем текст каждого варианта
                    for(j = 0; j < Entry->NumChoices; j++) {
                        GetNextQuotedLine(Text, fp, 2048);
                        Entry->Choices[j] = new char[strlen(Text) + 1];
                        strcpy(Entry->Choices[j], Text);
                    }
                }
            }
        }
    }
    fclose(fp);
    return TRUE;
}

Используя функцию cActionTemplate::Load вы открываете текстовый файл и начинаете сканировать его. В начале каждой итерации в новую структуру sAction загружается очередная строка в кавычках (действие), которая затем проверяется на наличие символов тильда. Если символы тильда найдены, загружается и разбирается остальная информация. Этот процесс продолжается пока не будет достигнут конец файла.

Перейдем к следующей функции в cActionTemplate — CreateScriptAction; она получает номер действия и возвращает инициализированную структуру sScript, подготовленную для хранения необходимого действию количества элементов. Затем вы можете непосредственно разбирать структуру sScript для доступа к данным, находящимся в действии и элементах (что делает редактор MLS и примеры):

sScript *cActionTemplate::CreateScriptAction(long Type)
{
    long i;

    sScript *Script;
    sAction *ActionPtr;

    // Проверяем, что это допустимое действие - тип
    // должен быть реально существующим ID действия
    // (из уже загруженного списка действий).
    if(Type >= m_NumActions)
        return NULL;

    // Получаем указатель на действие
    if((ActionPtr = GetAction(Type)) == NULL)
        return NULL;

    // Создаем новую структуру sScript
    Script = new sScript();

    // Устанавливаем тип и количество элементов
    // (выделяем список)
    Script->Type = Type;
    Script->NumEntries = ActionPtr->NumEntries;
    Script->Entries = new sScriptEntry[Script->NumEntries]();

    // Инициализируем каждый элемент
    for(i = 0; i < Script->NumEntries; i++) {
        // Сохраняем тип
        Script->Entries[i].Type = ActionPtr->Entries[i].Type;

        // Инициализируем данные элемента в зависимости от типа
        switch(Script->Entries[i].Type) {
          case _TEXT:
            Script->Entries[i].Text = NULL;
            break;
          case _INT:
            Script->Entries[i].lValue = ActionPtr->Entries[i].lMin;
            break;
          case _FLOAT:
            Script->Entries[i].fValue = ActionPtr->Entries[i].fMin;
            break;
          case _BOOL:
            Script->Entries[i].bValue = TRUE;
            break;
          case _CHOICE:
            Script->Entries[i].Selection = 0;
            break;
        }
    }
    return Script;
}

Последние две функции в cActionTemplate это ExpandDefaultActionText и ExpandActionText. Обе функции берут текст действия и заменяют символы тильда внутри него на более понятный текст, такой как целые числа или описание варианта выбора.

Различие между функциями в том, что ExpandDefaultActionText формирует текст не зависящий от данных элементов; она просто берет минимальное значение или первый вариант выбора. ExpandActionText формирует текст действия используя данные, содержащиеся в предоставляемой структуре sScript. Обе функции используются только в редакторе скриптов, чтобы сделать более удобными для просмотра данные, содержащиеся в шаблоне действий и структурах скрипта. Код функций вы можете посмотреть в программах на CD-ROM (в проекте MLS Script Editor).

ПРИМЕЧАНИЕ
Я не включил функции сохранения и загрузки скриптов, поскольку они не являются частью шаблона действий. Однако, вы можете модифицировать функции сохранения и загрузки для каждого приложения, как сочтете нужным. Это справедливо и для двух примеров программ к этой главе, MlsEdit и MlsDemo, которые находятся на прилагаемом к книге CD-ROM (обе программы расположены в каталоге \BookCode\Chap10).

Разобравшись с шаблонами действий и структурами скриптов вы можете начать объединять их вместе и применять MLS, а начинается все это с редактора скриптов Mad Lib.


netlib.narod.ru< Назад | Оглавление | Далее >

Сайт управляется системой uCoz