Перейти к содержанию

Классы и структуры

TODO

Необходимо обновить ссылки по всей статье.

ZScript является объектно-ориентированным языком, и, соответственно, в нём есть и понятия оттуда:

  • Класс: шаблон/штамп/описание/нечто неизменяемое. По классу создаются копии объектов. В классе лежит вся информация о том, что конкретно будет содержаться в объектах (различные переменные) и как оно будет взаимодействовать (функции, которые в ООП называются методами).
  • Объект (экземпляр класса): динамическая, "живая" копия класса. Является основным "действующим лицом" в ООП — клонировать их с одного класса можно сколь угодно много (...пока памяти хватает). То есть класс — форма для заливки, описание; объект — взаимодействие, динамика.
  • Структура: пользовательский составной тип данных и методов работы с ними.

Напомним, что обращение к какому-либо полю или методу класса производится посредством точки (см. 1.03, "Обращение по указателю").

Опциональность части синтаксиса

Во всех блоках кода ниже [Квадратные скобки] используются для того, чтобы обозначить необязательность написания того, что заключено внутри них.

Объвление структур и классов

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

m8f: Ненативные (не встроенные/самописные) структуры нельзя использовать в качестве параметров, также, как и возвращать их из функций. Но зато они легче классов.

Порядок блоков может варьироваться в зависимости от нужды. Фактически же он не имеет значения, обычно задаётся следующим образом:

struct имя_структуры {
    [константа1 = значение1;
    [...] ]

    [перечислимый_тип1;
    [...] ]

    [тип_переменной1 имя_переменной_структуры1;
    [...] ]

    [метод_структуры1;
    [...] ]
}
[...]

См. 1.04. "Константы и перечисляемые типы", 1.03. "Работа с переменными" и 1.10. "Методы".

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

class имя_класса [: от_кого_наследуется] {
    [константа1 = значение1;
    [...] ]

    [перечислимый_тип1;
    [...] ]

    [default-блок;] // Только в случае наследования от класса Actor или его потомков.

    [тип_переменной1 имя_переменной1;
    [...] ]

    [метод_класса1
    [...] ]

    [states-блок] // Только в случае наследования от класса Actor или его потомков.
}
[...]

Внутри классов также можно объявлять структуры, однако на практике это практически не используется. Объявить класс внутри класса или структуру внутри структуры уже нельзя.

Пример синтаксиса (скорее всего, будет куда-либо перенесён)

version "2.4.0"

#include "constants.zsc"        // Положим, здесь лежат второстепенные константы.
#include "zscript/weapons.zsc"
#include "zscript/monsters.zsc"
#include "zscript/npcs.zsc"

enum ENPCTeams {
    NPCT_Neutral = 0,
    NPCT_Normal = NPCT_Neutral, // ...Почему бы и нет?
    NPCT_Red,                   // == "NPCT_Neutral + 1", то есть "1".
    NPCT_Yellow,                // "2".
    NPCT_Green,
    NPCT_Blue, 
    NPCT_Gray,                  // "5".

    NPCT_LastTeam = NPCT_Gray
    // Чтобы каждый раз не вспоминать, какая там команда у нас последняя — делаем себе упрощение (здесь будет "NPCT_Gray", то есть "5").
}

class BaseNPC: Actor {
    ENPCTeams team;
    double bravefactor;

    Default {
        Monster;
        +FRIENDLY;
        Health 100;
    }

    override void BeginPlay() {
        team = Random( NPCT_Neutral, NPCT_LastTeam ); // Пользуемся мнемоническим упрощением...
        bravefactor = FRandom( 0.1, 1.0 );

        Super.BeginPlay();
    } // of override void BeginPlay() {}
}

Указатели на экземпляры классов (на объекты)

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

Указатель на себя

У каждого экземпляра класса есть автоматически создаваемый указатель self — буквально указатель на самого себя, на текущий объект, то есть на объект, код которого исполняется в данный момент.

Зачастую используется как аргумент в методе:

// Уменьшить здоровье своей цели, передав в качестве атакующего себя самого.
// Забавно и парадоксально, но подобный вызов используется нередко. Не отовсюду можно "попасть" в цель по-другому.

target.DamageMobj( self, self, 25, 'None' );

// Функция взята из "gzdoom.pk3::actor.txt":
//   native virtual int DamageMobj( Actor inflictor, Actor source, int damage, Name mod, int flags = 0, double angle = 0 );

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

class SomeClass {
    int a;
    double b;

    void Init( int a, String b = "" ) {
        self.a = a;
        self.b = 0.0;

        console.printf( b ); // Вот и думай, тут у нас как "double b" воспримется или как "String b".
                             //(Ответ — второе, как строка. Но сходу это совсем не очевидно).
    }
}

Особые правила для указателя self

Существуют специализированная обработка указателя self в классах, наследованных от StateProvider, а также запрет использования этого указателя в action-методах; см. статью 2.04 — "Actors".

Основополагающие типы классов

С каждым можно ознакомиться подробнее в соответствущей статье.

Actor
// Указатель на объект типа "Actor". Как раз акторами и являются все
//объекты в игровом мире -- игроки, монстры, оружие, декорации, 
//файерболы, дымок от пуль, источники динамического света и так далее.
// Класс "Actor" наследуется от класса "Thinker".

Thinker
// Thinker -- любой объект, имеющий право хоть как-то взаимодействовать
//с миром. Это, собственно, все акторы, эффекты секторов, "сущности" 
//ботов, декали, маркеры автокарты и иные объекты, прямо или косвенно
//связанные с игровым миром.

Object
// Самый базовый класс, несмотря на название "объект". Все (вообще
//все) остальные неявно наследуются от него.

ThinkerIterator
ActorIterator
// Вспомогательные объекты, позволяют перебрать всех Thinker'ов в мире,
//удовлетворяющих фильтру наследования.

BlockThingsIterator
BlockLinesIterator
// Вспомогательные объекты, используются для перебора всех линий или акторов,
//попадающих в BLOCKMAP-сетку вокруг определённого актора или определённой
//позиции в мире.

Дерево наследований объектов мира. Через "<...>" обозначены остальные, не основные классы. На наследование самописных это никак не влияет.

Object

  Thinker
  > Actor              // Decorate умеет работать только с акторами...
  |   Inventory        // Базовый класс инвентаря.
  |     StateProvider
  |       CustomInventory
  |       Weapon       // Базовый класс оружия.
  |         <...>
  |     Powerup        // Базовый класс Powerup'ов.
  |       <...>
  |     PowerupGiver
  |     <...>
  |   <...>
  > SectorEffect
  |   Lighting
  |   Mover
  |     MovingFloor
  |       Floor
  |     MovingCeiling
  |       Ceiling
  | <...>

  ThinkerIterator
  ActorIterator
  BlockThingsIterator
  BlockLinesIterator

  <...>

Создание объектов

Следует пользоваться двумя общими правилами:

1. Для того, чтобы создать объект класса, не наследованного от Actor (к примеру, Thinker), необходимо вызвать функцию "new()". Будет возвращён указатель на только что созданный объект этого типа.

Thinker th = new( 'Thinker' );

console.printf( "Thinker <" .. th .. "> created." ); // Будет выведен адрес, по которому был создан объект класса Thinker.
th.ChangeStatNum( STAT_STATIC );

2. Для создания в мире объекта типа Actor необходимо вызывать статический метод "Actor.Spawn( class<Actor> cls, vector3 spawnpos )" (какой актор создавать и на каких координатах).

Из этих двух правил есть несколько иключений. Например, для создания ThinkerIterator, ActorIterator, BlockThingsIterator, BlockLinesIterator, SectorTagIterator и других подобных предпочтительнее использовать их встроенные статические методы вроде "Create()", которые внутри себя проводят полную инициализацию и возвращают уже инициализированный объект. Такую же конструцкию можно делать и в самописных классах, если понадобится.