Герой после конкурса: WIP

» опубликован

Предисловие

Когда ZlaYa1000 организовывал конкурс героев, он спросил меня, буду ли я участвовать. Я ответил, что хотел бы, но не могу этого обещать. Как оказалось позже, интуиция меня не подвела, и в указанные сроки героя я предоставить не сумел.
Казалось бы, чего тут делать? "Взял несколько способностей, объединил их единой визуальной стилистикой, написал код - и вуаля! Карта готова!" Однако, не всё так просто.
Дело в том, что для того, чтобы придумать качественного, вписывающегося в стилистику "Dota 2" героя, которым было бы интересно играть и который бы не оказался чрезмерно сильным или чрезмерно слабым, нужно приложить некоторые усилия. Действительно стоящая идея почти никогда не приходит в числе первых, и чтобы до неё докопаться, нужно придумать и отсеять множество слабых концепций. Моя профессиональная гордость не позволяла мне взять первое, что придёт на ум, и когда в голове возник действительно стоящий герой, до конца конкурса оставалось чуть больше суток.
Набросав на скорую руку прототип, я пришёл к выводу, что сделать способности героя достаточно красивыми я не успеваю, и забросил всё это дело. Кто мог знать, что у меня была ещё такая прорва времени?..

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

Я не стану сразу целиком описывать механику героя, но после его завершения опубликую пост с длинной и витиеватой историей появления концепции (по сути это будет компиляция разбросанной по различным WIP'ам информации). Кроме того, в этой серии WIP-публикаций герой будет написан с нуля, без использования созданного ранее прототипа.
В реализации используется cJass, версии на vJass не планируется.

Собственно, WIP

Мне очень хотелось, чтобы все способности героя были каким-либо образом связаны между собой и не были просто набором механик (как это есть, например, у Lion'а). Это моё желание стало основной проблемой при создании концепции: вариантов было много, но среди них не находилось героя, который по моей оценке подходил бы к стилистике Dota 2.
Тем не менее, постепенно я пришёл к тому, что герой будет основываться на механике, чем-то похожей на механику Earth Spirit: одна способность создаёт на карте некую сущность, а все остальные эту сущность используют. В ходе окончательной формулировки концепции я пришёл к тому, что остальные способности вообще не могут быть применены, если на карте отсутствуют сущности, созданные кастующим героем. Также были приняты следующие решения: никаких зарядов (как у камней Earth Spirit) эта способность использовать не должна; эта способность должна быть ультимейтом героя. Подобное решение привело к тому, что на первых этапах игры перезарядка ультимейта по сути оказалась глобальным кулдауном способностей героя, вынудив меня искать способы усилить его на первых этапах игры, не сделав при этом слишком сильным на поздних.
Из всех возможных вариантов я остановился на следующих:
  • Первый уровень ультимейта изучен у героя по-умолчанию и не требует отдельного очка навыков
  • Порождаемая ультимейтом сущность должна обладать небольшим обзором
  • Радиус обзора также показывает зону действия способности, применённой к сущности
  • Эффективность способностей должна зависеть от того, как долго живёт целевая сущность
  • Сущности неуязвимы и видимы только союзникам игрока-владельца
Определившись с основными моментами, я запустил редактор и набросал следующие элементы:
  • Небольшую библиотеку для удобного создания периодических действий
  • Библиотеку для удобного создания локальных эффектов
  • Библиотеку для удобной регистрации событий применения и изучения способностей
Плюс, сделал болванку дамми-юнита для создаваемой сущности (на самом деле, скопировал универсальную с костью "origin" и кучей анимаций поворота).
» library Timer
library Timer uses Config {
    #include "cj_types_priv.j"
    #include "cj_typesEX_priv.j"

    private timer GameClock = new timer;

    void onEachTick(code actionfunc) {
        TimerStart(GameClock, FRAME_TIME, true, actionfunc);
    }
}
» library LocalEffect
library LocalEffect {
    #include "cj_types_priv.j"
    #include "cj_typesEX_priv.j"

    private force  allies = new force;
    private string temp;

    private string ForAllies(player ofPlayer, string modelName) {
        temp = " ";
        ForceEnumAllies(allies, ofPlayer, null);
        if (IsPlayerInForce(GetLocalPlayer(), allies)) {
            temp = modelName;
        }
        ForceClear(allies);
        return temp;
    }

    effect AddEffectForAllies(player ofPlayer, string modelName, float x, float y) {
        return AddSpecialEffect(ForAllies(ofPlayer, modelName), x, y);
    }

    effect AddEffectTargetForAllies(player ofPlayer, string modelName, widget targetWidget, string attachPointName) {
        return AddSpecialEffectTarget(ForAllies(ofPlayer, modelName), targetWidget, attachPointName);
    }
}
» library OnSpell
library OnSpell {
    #include "cj_types_priv.j"
    #include "cj_typesEX_priv.j"

    private void registrator(boolexpr condition, code actionFunc, playerunitevent which) {
        trigger trig = new trigger;
        int i = 15;
        while (i >= 0) {
            TriggerRegisterPlayerUnitEvent(trig, Player(i), which, null);
            i--;
        }
        TriggerAddCondition(trig, condition);
        TriggerAddAction(trig, actionFunc);
        trig = null;
    }

    void onSpellLearn(boolexpr condition, code actionFunc) {
        registrator(condition, actionFunc, EVENT_PLAYER_HERO_SKILL);
    }

    void OnSpellEffect(boolexpr condition, code actionFunc) {
        registrator(condition, actionFunc, EVENT_PLAYER_UNIT_SPELL_EFFECT);
    }
}
Также заранее выписал все настраиваемые параметры:
» library Config
library Config {
    #include "cj_types_priv.j"

    // For export:
    define Q_SPELL_ID = 'a000';
    define W_SPELL_ID = 'a001';
    define E_SPELL_ID = 'a002';
    define R_SPELL_ID = 'AOsw';
    define HERO_UNIT_ID = 'Oshd';
    define DUMMY_UNIT_ID = 'u000';  // Dummy unit should be configured as in this map;

    // Ability visual configuration:
        // New spawn;
    define ULT_MODEL = "Abilities\\Spells\\Undead\\Darksummoning\\DarkSummonTarget.mdl";
        // Fully charged spawn;
    define ULT_MODEL_FULL = "Abilities\\Spells\\Undead\\DarkSummoning\\DarkSummonMissile.mdl";

    // System params:
    define MAX_HEROES = 32;
    define MAX_SPAWNS = 160;    // SpawnLimit[maxLvl] * MAX_HEROES;
    define FRAME_TIME = 0.04;   // For periodical events;

    // Hero balance:
    define AOE_RADIUS = 256.0;  // Sight radius of dummy and his area of effect;
    define CHARGE_MAX = 100.0;  // Used as percents, can be set separately for levels;

    int SpawnLimit[];       // SpawnLimit[n] >= SpawnLimit[n-1], n >= 1; n - ultimate level.
    float ChargeTime[];

    void InitHeroConfig() {
        SpawnLimit[1] = 1;
        SpawnLimit[2] = 2;
        SpawnLimit[3] = 3;
        SpawnLimit[4] = 5;

        ChargeTime[1] = 45.0;
        ChargeTime[2] = 30.0;
        ChargeTime[3] = 15.0;
        ChargeTime[4] =  7.5;
    }
}

Теперь настала пора описать сущность. Поскольку все сущности со временем усиливаются, нужно было каким-то образом хранить их в одном месте (чтобы без усилий пробегаться по ним и увеличивать заряд), а чтобы не было нужды при каждом изменении максимального количества сущностей лезть в код и расширять массив у героя, связь между сущностями одного героя сделал по схеме (паттерну, whatever) LinkedList. Это значит, что в каждом объекте списка хранится ссылка на следующий объект этого списка; в моей случае хранятся ссылки на оба соседних элемента (предыдущий и следующий).
» library Spawn
library Spawn uses Timer, Config, LocalEffect {
    #include "cj_types_priv.j"

    struct Spawn
    {
        public int level;           // Owning hero ultimate level;
        public Spawn prev, next;    // Links to list members;
        public float x, y;          // Coordinates;
        private float charge;       // Current charge;
        private unit dummy;         // Link to representing unit;
        private effect model;       // Link to representing model;

        private static Spawn spawnList[MAX_SPAWNS];
        private static int spawnCount = 0;

        // Coordinates, casting unit, level of ultimate, previous spawn of casting hero;
        public static thistype create(float x, float y, unit hero, int level, Spawn prevSpawn) {
            thistype this = thistype.allocate();
            if (prevSpawn != 0) {       // Check if here is no previous spawn;
                this.prev = prevSpawn;  // Write links between spawns;
                prevSpawn.next = this;
            }
            this.level = level;
            this.x = x;
            this.y = y;
            this.dummy = CreateUnit(GetOwningPlayer(hero), DUMMY_UNIT_ID, this.x, this.y, 0.0);
            SetUnitAnimationByIndex(this.dummy, 0);
            this.model = AddEffectTargetForAllies(GetOwningPlayer(hero), ULT_MODEL, this.dummy, "origin");
            this.charge = 0.0;
            this.next = 0;
            if (spawnCount < MAX_SPAWNS) {      // Check if here isn't too many spawns; 
                spawnCount++;
                spawnList[spawnCount] = this;   // Add spawn to global list of spawns;
            }
            return this;
        }

        private void removeFromList() {
            int i = spawnCount;
            while (spawnList[i] != this) {          // Searching for this spawn id in global list;
                i--;
            }
            spawnList[i] = spawnList[spawnCount];   // Removing this spawn from global list;
            spawnList[spawnCount] = 0;
            spawnCount--;
        }

        public void destroy() {
            removeFromList();
            DestroyEffect(this.model);
            this.model = null;
            RemoveUnit(this.dummy);
            this.dummy = null;
            if (this.next != 0) { this.next.prev = this.prev; }
            if (this.prev != 0) { this.prev.next = this.next; }
            this.deallocate();
        }

        // Used in case where casting hero already have maximum spawns;
        public void destroyFirst() {
            thistype temp = this;
            while (temp.prev != 0) {
                temp = temp.prev;
            } 
            temp.destroy();
        }

        private void update() {
            if (this.charge < CHARGE_MAX) {
                this.charge += CHARGE_MAX/ChargeTime[this.level]*FRAME_TIME;
                if (this.charge >= CHARGE_MAX) {
                    DestroyEffect(this.model);
                    this.model = AddEffectTargetForAllies(GetOwningPlayer(dummy), ULT_MODEL_FULL, this.dummy, "origin");
                    this.charge = CHARGE_MAX;
                }
            }
        }

        private static void updateAll() {
            int id = spawnCount;
            while (id > 0) {
                spawnList[id].update();
                id--;
            }
        }

        static void initialize() {
            onEachTick(function Spawn.updateAll);
        }
    }
}
На это пока что всё, до следующих встреч!

Вопросы и комментарии приветствуются.


Просмотров: 225

Diaboliko #1 - 3 месяца назад 0
Привяжи переменную, содержащую номер ячейки, являющейся началом части целочисленного массива, выделенного для героя к герою через unitiserdata и выдели константное число элементов под сущности этого героя и прогоняйся по ним циклом. Это вместо двусвязного списка. Для хранения иных значений можно использовать оффсеты номера ячейки или иные массивы с параллельным выделением номеров
Clamp #2 - 3 месяца назад 0
Diaboliko, спасибо за коммент, сейчас отвечу по частям.

выдели константное число элементов под сущности
Оно так и есть, в Config выведена константа (через define).
unitiserdata...вместо двусвязного списка
Двусвязный список в данном случае ничуть не хуже, и даже лучше, так как не использует внешних игровых сущностей.
Для хранения иных значений можно использовать оффсеты номера ячейки или иные массивы с параллельным выделением номеров
Это по своей сути будет просто огромным костылём и существенно усложнит архитектуру кода в целом без каких-либо реальных улучшений.

Как-то так =)
Diaboliko #3 - 3 месяца назад 0
Чтож, я привык к описанному способу и нынче мне он кажется удобным :)
Clamp #4 - 3 месяца назад 0
нынче мне он кажется удобным :)
Это до тех пор, пока ты не пишешь вне варкрафта =)

Попробуй подход, который тут описан, не пожалеешь =Р
Doc #5 - 3 месяца назад 0
Героя посмотрю как вернусь с ТИ, но сразу скажу: придумать героя у которого все способности связаны-перевязаны проще всего, и я считаю что лион будет интереснее 99% героев завязанных на одной сущности и к тому же не могущих без нее существовать. Но это так, прелюдия. Интересный ли герой еще предстоит увидеть. То что я описал выше - не предвзятость, а скорее исходит из персонального опыта.
Sylvanas #6 - 3 месяца назад (отредактировано ) 0
Я думаю что набор способностей героя которые между собою связаны является показателем специализации героя в каком-то русле. Ну кто назовет нормальным то что маг огня использует ледяную магию?
Clamp #7 - 3 месяца назад 0
Героя посмотрю как вернусь
Это не особо скоро, как я понимаю? Герой ATM доделан не до конца, как видно из надписи "WIP", но к концу TI, полагаю, будет завершён.
Хотя бы код критиковать будешь до этого? :D
сразу скажу: придумать героя у которого все способности связаны-перевязаны проще всего [...]
С механиками и пониманием юзабилити у меня пока что вроде как неплохо, всё-таки давно уже не любитель.
Энивей, спасибо за замечание =)

Sylvanas, если честно, то даже не знаю что и ответить =/
Doc #8 - 3 месяца назад 0
У меня лаптопа даже нет с собой. ТИ кончается через неделю, так что примерно тогда.