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

Сабж

Наверняка вы при желании создать РПГ сталкивались с мыслью, "как же оживить города в варике как в том же скайриме, или любой другой стандартной рпг"?
Некоторые пытались это сделать, и достигали опреденных успехов, как мы видели в Gods Word (xgm.guru/p/gw), однако подход там был топорный, и руководствовался стандартным мышлением по типу "один житель - десяток триггеров", которые выверяли путь который должен пройти "житель", и время когда он должен по ним проходить. Плюс сюда можно прибавить еще несколько триггеров, которые отвечали за имитацию деятельности.
Конечно, это несомненно рабочий подход с точки зрения формальной логики, однако с практической точки зрения это самый неэффективный способ. Мало того, что вам потребуется создать десятку-сотню триггеров, ориентироваться потом во всем этом будет просто нереально. Учитывая нагрузку на сам редактор, чем больше мы попробуем сделать так жителей, тем больше это походит на ашота-строителя. И кстати такой подход выбирает подавляющее количество новичков в редакторе
Как завещал нам Стив Джобс - РАБОТАТЬ НУЖНО ГОЛОВОЙ, А НЕ 8 ЧАСОВ

Что же делать?

Попробуем создать примитивную деревню с 4 жителями, которые ходят на свою работу, и уходят после нее домой, чисто для примера подхода
Если когда либо какая то задача вам покажется невыполнимой - разбейте ее на несколько составляющих
И наша задача "сделать живую деревню без сотни триггеров" разобьется на несколько
  • организация хранения данных жителей, можно назвать это паспортом, где живет, где работа, когда выходит на нее и когда уходит
  • набор разнообразных функций связанных с деревней
  • система поиска пути
Примерно будет такое взаимодействие
Условно, данные жителей берут функции деревни, и по результатам могут отправить их по определенному пути. Итого у нас уже выстроилась зависимость и определенные блоки которые нужно реализовать!

Хранения данных в варкрафте разнообразны. В данном случае, я буду говорить про версию 1.26а.
Имеются у нас такие варианты
  • Хештаблица
  • Структуры
  • Параллельные массивы
важное замечание
структуры из vJass являются как раз параллельными массивами, просто в удобной обертке

Хештаблицы не очень практичны для данного случая, а данный случай - база данных. Параллельные массивы - это как раз обычные массивы, только один индекс массива во всех нужных нам массивах символизирует индекс объекта.
Таким образом через параллельные массивы можно реализовывать МУИ заклинания через ГУИ!

Параллельные массивы


Визуальный пример параллельных массивов

	unit Owner[10]
	real Damage[10]
	integer MoveSpeed[10]
Здесь 3 массива с разными данными, но объединяет их один индекс! Получив индекс одного типа данных, мы сразу же имеем доступ ко всем остальным
Owner[2] = gg_unit_Hjai_0002
Damage[2] = 30.
MoveSpeed[2] = 100
По индексу 2 у нас будет юнит с нашим "уроном" 30, и "скоростью" 100
На индексе 1 уже будет другой юнит со своими данными, на 3 4 5 6 и т.д. свои данные и свои юниты
Проходясь циклом от 1 до максимального индекса данных массивов, вы пройдетесь по всей вашей "базе данных". И таким образом сможете делать с ними что душе угодно!


"​Паспорта" наших жителей

Здесь используется cJass, т.к. версия на данный момент использовалась 1.26а. Но в актуальных версиях данный define можно будет заменить через constant integer Citizens = 10
код
define private Citizens = 10
    
    public string Profession[Citizens] //сделано чисто для нас, в данном случае не имеет практической пользы
    public unit Citizen[Citizens] // сам юнит
    public rect Work[Citizens] // область с его работой
    public rect Home[Citizens] // область с его домом
    public real WakeUpTime[Citizens] // во сколько просыпается
    public real EndWorkTime[Citizens] // во сколько идет домой
    private int State[Citizens] // текущее состояние жителя
    public int Amount = 4 // количество жителей
Здесь у нас параллельные массивы с размером 10. Это значит, мы можем хранить до 10 жителей, но использоваться будет только 4, что мы и указали в Amount.
важный момент
State[Citizens] принимает несколько состояний основанных на целочисленных
состояния у на будут перечислены так
enum (UNIT_STATE) { NO_STATE, AT_HOME, GOING_TO_WORK, WORKING, GOING_TO_HOME }
энум пронумеровывает каждое состояние своим числом начиная с нуля, это позволит нам делать такие конструкции как State[1] = GOING_TO_WORK, что позволит нам динамично менять состояние жителя или проверять в любой момент какое оно.
тоже можно заменить константами

Теперь нужно эту базу заполнить по имеющемуся шаблону
в любой функции инициализации
		Village_Profession[1] = "Farmer"
        Village_Citizen[1] = gg_unit_nvil_0005
        Village_Work[1] = gg_rct_node_13
        Village_Home[1] = gg_rct_node_28
        WakeUpTime[1] = 7.
        EndWorkTime[1] = 19.
        State[1] = AT_HOME
        
        Village_Profession[2] = "Trader"
        Village_Citizen[2] = gg_unit_nvl2_0004
        Village_Work[2] = gg_rct_node_24
        Village_Home[2] = gg_rct_node_27
        WakeUpTime[2] = 9.
        EndWorkTime[2] = 17.
        State[2] = AT_HOME
        
        Village_Profession[3] = "Trader 2"
        Village_Citizen[3] = gg_unit_nvlw_0006
        Village_Work[3] = gg_rct_node_22
        Village_Home[3] = gg_rct_node_26
        WakeUpTime[3] = 8.
        EndWorkTime[3] = 18.3
        State[3] = AT_HOME
        
        Village_Profession[1] = "Blacksmith"
        Village_Citizen[4] = gg_unit_hpea_0011
        Village_Work[4] = gg_rct_node_20
        Village_Home[4] = gg_rct_node_25
        WakeUpTime[4] = 7.4
        EndWorkTime[4] = 19.
        State[4] = AT_HOME

я использовал глобальные ссылки на юнитов которые уже стояли в редакторе. хехе
а находится это все в функции инициализации триггера
function InitTrig_Village takes nothing returns nothing

		Village_Profession[1] = "Farmer"
        Village_Citizen[1] = gg_unit_nvil_0005
		....
база будет заполнена при инициализации карты. хехехе. удобно, правда?

Поиск пути

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

Вкратце, мой алгоритм проверяет соседние графы из текущего которые уменьшат расстояние до конечной, при этом игнорируя уже пройденные графы, а при тупике вернется назад и проверит другой путь.
Главное, что у нас появились новые функции
Path FindPathForUnit(unit u, int order, rect end) 

возвращает структуру с путем графов который будет построен, а так же посылает юнита с приказом по данному пути если он найден

еще один новый объект это структура Node
создается как
Node new_node_1 = Node.new(gg_rct_node_1)

и затем нам нужно присоединить к нему другой граф
new_node_1.connect(DIRECTION_UP_RIGHT, new_node_2)

указываем направление (для своего удобства) и другой граф к которому нужно присоединить

только надо помнить, что такая связь - односторонняя, со стороны графа node 2 нужно тоже присоединить граф node 1 если это необходимо

так проделываем со всеми нужными графами
Так у нас есть инструмент посылки юнитов по составленному пути. Накидал на скорую руку, но вроде более менее работает. Данное не для новичков, но можете поковырять, снимая комментарии и смотря на данные которые выводятся.

Деревня

Теперь у нас есть "средство передвижения", и "паспорта" жителей.
Осталось обрамить это все в одно взаимодействие.
Но в целом, дальше все - довольно тривиально. Мы проверяем время дня, и отправляем жителей на работу с помощью системы путей до работы, а при наступлении времени отдыха, так же возвращаем их домой
код
	private void Daytime(){
        real time = GetFloatGameState(GAME_STATE_TIME_OF_DAY)
        int i = 0
        
            while(i++ < Amount){
                if ((time >= WakeUpTime[i] and time < EndWorkTime[i]) and State[i] == AT_HOME) {
                    SetUnitX(Citizen[i], GetRectCenterX(Home[i]))
                    SetUnitY(Citizen[i], GetRectCenterY(Home[i]))
                    ShowUnit(Citizen[i], true)
                    State[i] = GOING_TO_WORK
                    FindPathForUnit(Citizen[i], order_move, Work[i])
                }
                elseif ((time < WakeUpTime[i] or time >= EndWorkTime[i]) and State[i] == WORKING) {
                    State[i] = GOING_TO_HOME
                    FindPathForUnit(Citizen[i], order_move, Home[i])
                    msg("issue " + I2S(i) + " to go home")
                }
            }
            
    }

	.......
	TimerStart(CreateTimer(), 0.5, true, function Daytime)
	
Обратите внимание на использование параллельных массивов из базы данных которую мы сделали заранее. Просто проходимся по каждому элементу и проверяем их.

При вхождении в область работы, жителя перехватит дугой триггер, который посмотрит состояние жителя, и кинет его по функциям работы
Код
private void EnterWork(){
        int index = GetIndex(GetEnteringUnit())
        
            if State[index] == GOING_TO_WORK {
                if IsUnitInRangeXY(Citizen[index], GetRectCenterX(Work[index]), GetRectCenterY(Work[index]), 150.) {
                    State[index] = WORKING
                    
                        if GetEnteringUnit() == Citizen[1] {
                            TimerStart(CT, 0.6, false, function FarmerMoving)
                        }
                        elseif GetEnteringUnit() == Citizen[2] {
                            TimerStart(CT, 0.6, false, function Shoutout1)
                        }
                        elseif GetEnteringUnit() == Citizen[3] {
                            TimerStart(CT, 0.6, false, function Shoutout2)
                        }
                        elseif GetEnteringUnit() == Citizen[4] {
                            TimerStart(CT, 0.6, false, function Forging)
                        }
                        
                }
            }
    }
проверяется расстояние до работы что бы убедиться что это именно его место работы

Сами функции "работы"
код

private void FarmerMoving(){
            if IsWorkTime(1) {
                IssuePointOrderById(Citizen[1], order_move, RndR(GetRectMinX(gg_rct_FIELD), GetRectMaxX(gg_rct_FIELD)), RndR(GetRectMinY(gg_rct_FIELD), GetRectMaxY(gg_rct_FIELD)))
                TimerStart(GetExpiredTimer(), RndR(4., 8.), false, function FarmerMoving)
            }
            else {
                DT(GetExpiredTimer())
            }
        
    }
    
    private void Shout(unit from, string text, real size,real for){
        bj_lastCreatedTextTag = CreateTextTag()
        SetTextTagText(bj_lastCreatedTextTag, text, (size * 0.023) / 10)
        SetTextTagPosUnit(bj_lastCreatedTextTag, from, 7.)
        SetTextTagColor(bj_lastCreatedTextTag, 225, 225, 255, 0)
        SetTextTagPermanent(bj_lastCreatedTextTag, false)
        SetTextTagLifespan(bj_lastCreatedTextTag, for)
        SetTextTagFadepoint(bj_lastCreatedTextTag, 0.25)
    }
    
    
    private void Shoutout1(){
        if IsWorkTime(2) {
            SetUnitFacing(Citizen[2], 90. + RndR(-20., 20.))
            TimerStart(GetExpiredTimer(), RndR(4., 8.), false, function Shoutout1)
            Shout(Citizen[2], "Пятерочка вас кинет, заходите к нам в Ашан!", 10., 2.7)
        }
        else {
            DT(GetExpiredTimer())
        }
    }
    
    private void Shoutout2(){
        if IsWorkTime(3) {
            SetUnitFacing(Citizen[3], 90. + RndR(-20., 20.))
            TimerStart(GetExpiredTimer(), RndR(4., 8.), false, function Shoutout2)
            Shout(Citizen[3], "Сеть лавок ''Пятерочка'' ждет своих покупателей!", 10., 2.7)
        }
        else {
            DT(GetExpiredTimer())
        }
    }
    
    private void Forging() {
        if IsWorkTime(4) {
        
                if RndI(1, 2) == 1 {
                    SetUnitFacing(Citizen[4], 149.)
                    SetUnitAnimation(Citizen[4], "stand work")
                }
                else {
                    SetUnitFacing(Citizen[4], 149. - (180. + RndR(-10., 10.)))
                    SetUnitAnimation(Citizen[4], "stand")
                    Shout(Citizen[4], "Как же я заебался...", 10., 2.7)
                }
                
            TimerStart(GetExpiredTimer(), RndR(4., 8.), false, function Forging)
        }
        else {
            DT(GetExpiredTimer())
        }
    }

Собственно, кое каких результатов мы достигли! При желании, можно углублять как угодно работу, вводить новые состояния, и делать проверки на них, добавить разброс в выходе и уходе с работы, добавлять перерывы (опять же состояниями), и это не будет занимать десятки триггеров. Можно разбить данные функции на два триггера, и это будет все еще оптимально.

Карта в закрепе, вопросы в комменты или лс, воры в законе, тюлени на хгм!
`
ОЖИДАНИЕ РЕКЛАМЫ...

Показан только небольшой набор комментариев вокруг указанного. Перейти к актуальным.
3
21
5 лет назад
3
В самом деле это валидный подход не только в рамках модмейкинга. Эдакий урок здравого смысла.
0
27
5 лет назад
0
Вспомнилось, как ковырял SandBox в скайрим
0
20
5 лет назад
0
Как следующий этап данной разработки, можно сделать ИИ, который будет прокладывать путь по ребрам.
0
18
5 лет назад
0
А чего в блог, а не в статьи?
2
26
5 лет назад
2
Кристофер:
А чего в блог, а не в статьи?
наверное на статью особо не тянет, не знаю, если будет солидарное мнение можно сделать статью, сначала решил сделать в блог хотя бы
PhysCraft:
Как следующий этап данной разработки, можно сделать ИИ, который будет прокладывать путь по ребрам.
ИИ вообще заслуживает отдельного блока) по сути ему нужно будет выбирать точку которая наиболее близкая к конечной, а алгоритм достроит за него путь. Можно сделать базовый ИИ для жителя, в котором будут условные потребности, типа, поработал вот он какое то время, и чем выше усталость, тем вероятнее он пойдет отдохнуть, или учет материалов если это кузнец, наработал материал, пошел сложил на склад или торговцу
а кстати я даже больше скажу насчет пути, можно добавить на каждый граф свой приоритет, и затем искать пути с определенным приоритетам
допустим главные дороги с приоритетом 3, всякие небольшие отклонения от дороги имеют приоритет 2, а самые далекие не идущие рядом с цивилизацией 1
пускаем жителя по приоритету 3, он идет по дороге т.к. выбирает только такой приоритет, пускаем разбойника по приоритету 1, и он старается обойти все оживленные места
2
29
5 лет назад
2
Статья хороша, но всё равно как бы ты ни старался, найдутся те, кто сделают всё по своему, а потом будут репу чесать.
0
30
5 лет назад
0
Очевидный пример применения абстракций. Все жители деревни по сути есть одно и то же, но с разными настройками, почему бы и не воспользоваться единым методом?
Одним из очевидных улучшений будет добавление случайного действия, например захода в трактир после работы или диалогов между жителями при встрече.
0
26
5 лет назад
0
Clamp:
Очевидный пример применения абстракций. Все жители деревни по сути есть одно и то же, но с разными настройками, почему бы и не воспользоваться единым методом?
Одним из очевидных улучшений будет добавление случайного действия, например захода в трактир после работы или диалогов между жителями при встрече.
я решил взять самые очевидные и простые методы. улучшать так то можно как угодно, все зависит от фантазии, просто для конкретно данного случая решил не усложнять особо
0
26
5 лет назад
0
Если сохранять:
Если отключить cJass:
Загруженные файлы
Показан только небольшой набор комментариев вокруг указанного. Перейти к актуальным.
Чтобы оставить комментарий, пожалуйста, войдите на сайт.