Население мира л2

DaVilka

Интересующийся
Местный
#1
В мире много мобов и нпц, как они живут со стороны сервера? :-) Не создается же отдельный поток для каждого моба и нпц. По какой технологии они двигаются?
 

HostMan

Интересующийся
Местный
#2
Сам не вникал, но судя по всему - создается. Но происходит это исключительно для объектов в радиусе видимости.
На самом деле интересный вопрос, поэтому если неправ, - интересно узнать и самому)
 

Howli

Участник
Пользователь
#3
Могу описать принцип работы с тех времен, когда анализировали украденную версию С4 (или ц5 точно уже не помню) написанную на Си. Как работает на джаве - не вкурсе, может быть аналогично, хотя сомнительно.
Карта разбита на определенные области. Монстры каждой области при старте сервака начинают грузится в память, по очередности.
После полной загрузки, начинается периодический запуск (тик) просчета областей.
Каждая область имеет состояние активности. Если в этой области не находится ни одного игрока - то область помечается не активной. Если область не активна то все просчеты монстров в ней не выполняются. Монстры не двигаются, ничего не делают, ничего не происходит. Даже респа мобов нет.
Если область активна - то от каждого игрока в данной области вычисляется радиус до монстра (возможно бьется на дополнительные области), если радиус вписывается в видимость монстра - то монстру ставится в цель ваш ид персонажа. Дальше идет работа алгоритма порядка ид-ов цели, поиск пути к координатам, и атака вас если координаты сблизились. Так же запоминается время добавление вашего ид в список. Если монстр не смог вас настигнуть за 2 минуты (вроде там было 2) то ваш ид убирается из списка монстра, и монстр идет на изначальную точку с каждым тиком расчета. Конечно, если вы его троните - то ваш ид опять попадает в список целей. Видимость у монстров сделана довольно тупо - попадание радиус, и не важно вы стоите за стеной, домом или хоть в клан холе.
Если хп монстра падает до 0, то мобу ставится определенный статус, и выставляется временная метка респа. Помню важный момент, монстр остается в той же ячейке памяти, только изменяется его состояние. Если последующие циклы обработки монстров попадают на время респа меньше текущего - то мобу выставляется статус и респится моб.
Про сокеты. Каждое действие, например монстр задумал сделать пару шагов, попадет в определенный пул действий текущего цикла. Все игроки которые подключены к серверу, получают пакеты из этого пула в пределах определенного радиуса. И вы тем самым видите как монстр пошел. Но вы ничего не сможете увидеть дальше радиуса, потому что сервер вам просто ничего не сообщает более. Логично изменяя свои координаты вы начинаете получить другие действия из пула.
Приблизительно так выходит если говорит простыми словами. Технически конечно намного сложнее...
 

DaVilka

Интересующийся
Местный
#4
Сам не вникал, но судя по всему - создается. Но происходит это исключительно для объектов в радиусе видимости.
На самом деле интересный вопрос, поэтому если неправ, - интересно узнать и самому)
Это если на серве 500+ человек, и все розкиданы по всему миру, то это же писос скоко потоков будет. У меня есть теория на этот счет.
Могу описать принцип работы с тех времен, когда анализировали украденную версию С4 (или ц5 точно уже не помню) написанную на Си. Как работает на джаве - не вкурсе, может быть аналогично, хотя сомнительно.
Карта разбита на определенные области. Монстры каждой области при старте сервака начинают грузится в память, по очередности.
После полной загрузки, начинается периодический запуск (тик) просчета областей.
Каждая область имеет состояние активности. Если в этой области не находится ни одного игрока - то область помечается не активной. Если область не активна то все просчеты монстров в ней не выполняются. Монстры не двигаются, ничего не делают, ничего не происходит. Даже респа мобов нет.
Если область активна - то от каждого игрока в данной области вычисляется радиус до монстра (возможно бьется на дополнительные области), если радиус вписывается в видимость монстра - то монстру ставится в цель ваш ид персонажа. Дальше идет работа алгоритма порядка ид-ов цели, поиск пути к координатам, и атака вас если координаты сблизились. Так же запоминается время добавление вашего ид в список. Если монстр не смог вас настигнуть за 2 минуты (вроде там было 2) то ваш ид убирается из списка монстра, и монстр идет на изначальную точку с каждым тиком расчета. Конечно, если вы его троните - то ваш ид опять попадает в список целей. Видимость у монстров сделана довольно тупо - попадание радиус, и не важно вы стоите за стеной, домом или хоть в клан холе.
Если хп монстра падает до 0, то мобу ставится определенный статус, и выставляется временная метка респа. Помню важный момент, монстр остается в той же ячейке памяти, только изменяется его состояние. Если последующие циклы обработки монстров попадают на время респа меньше текущего - то мобу выставляется статус и респится моб.
Про сокеты. Каждое действие, например монстр задумал сделать пару шагов, попадет в определенный пул действий текущего цикла. Все игроки которые подключены к серверу, получают пакеты из этого пула в пределах определенного радиуса. И вы тем самым видите как монстр пошел. Но вы ничего не сможете увидеть дальше радиуса, потому что сервер вам просто ничего не сообщает более. Логично изменяя свои координаты вы начинаете получить другие действия из пула.
Приблизительно так выходит если говорит простыми словами. Технически конечно намного сложнее...
Ахренеть, почти так как я придумал, я думал поднять поток каждому квадрату, который проверял бы наличие в нем игроков, если игроков несколько, то высчитывался бы минимальные и максимальные координаты радиуса игроков, добавлять эти координаты в массив текущего квадрата(и соседних, если радиус их затрагивает), и уже поток квадрата относительно координатам радиуса рисовал бы мобов, раз в какое то время проверял бы видимых мобов, нет ли у них желания пройтись или разовнятся, а уже заагреному мобу поднимать отдельный поток(или всех заагреных мобов в радиусе обрабатывать одним потоком , не решил)
 

lordofdest

Интересующийся
Партнер
#5
Монстры не двигаются, ничего не делают, ничего не происходит.
Насколько помню тик нпц всеравно отрабатывает всегда и нпц по прежнему пытает выполнить свои действия но они фильтруются в случаи если квадрат не активен.
 

KanuToIIIKa

Пляшущий с бубном
Местный
#6
Овероподобные сборки:
Собственно когда игровому объекту (нпц, игроки, предметы и т.д.) присваивается позиция (setXYZ)
происходит следующее:
Код:
public void setXYZ(int x, int y, int z)
    {
        _x = World.validCoordX(x);
        _y = World.validCoordY(y);
        _z = World.validCoordZ(z);

        World.addVisibleObject(this, null);
    }
Нас интересует вызов addVisibleObject пойдем дальше именно дальше происходит "магия"
Код:
    public class ActivateTask extends RunnableImpl
    {
        private boolean _isActivating;

        public ActivateTask(boolean isActivating)
        {
            _isActivating = isActivating;
        }

        @Override
        public void runImpl() throws Exception
        {
            if(_isActivating)
                World.activate(WorldRegion.this);
            else
                World.deactivate(WorldRegion.this);
        }
    }

    public static void addVisibleObject(GameObject object, Creature dropper)
    {
        if(object == null || !object.isVisible())
            return;

        final WorldRegion region = getRegion(object);
        final WorldRegion currentRegion = object.getCurrentRegion();

        if(currentRegion == region)
            return;

        int x1, y0, y1, z0, z1;
        if(currentRegion == null) // Новый обьект (пример - игрок вошел в мир, заспаунился моб, дропнули вещь)
        {
            // Добавляем обьект в список видимых
            object.setCurrentRegion(region);
            region.addObject(object);

            // Показываем обьект в текущем и соседних регионах
            // Если обьект игрок, показываем ему все обьекты в текущем и соседних регионах
            x1 = validX(region.getX() + 1);
            y0 = validY(region.getY() - 1);
            y1 = validY(region.getY() + 1);
            z0 = validZ(region.getZ() - 1);
            z1 = validZ(region.getZ() + 1);
            for(int x = validX(region.getX() - 1); x <= x1; x++)
                for(int y = y0; y <= y1; y++)
                    for(int z = z0; z <= z1; z++)
                        getRegion(x, y, z).addToPlayers(object, dropper);
        }
        else
        // Обьект уже существует, перешел из одного региона в другой
        {
            currentRegion.removeObject(object); // Удаляем обьект из старого региона
            object.setCurrentRegion(region);
            region.addObject(object); // Добавляем обьект в список видимых

            // Убираем обьект из старых соседей.
            int rx = region.getX();
            int ry = region.getY();
            int rz = region.getZ();
            x1 = validX(currentRegion.getX() + 1);
            y0 = validY(currentRegion.getY() - 1);
            y1 = validY(currentRegion.getY() + 1);
            z0 = validZ(currentRegion.getZ() - 1);
            z1 = validZ(currentRegion.getZ() + 1);
            for(int x = validX(currentRegion.getX() - 1); x <= x1; x++)
                for(int y = y0; y <= y1; y++)
                    for(int z = z0; z <= z1; z++)
                        if(!isNeighbour(rx, ry, rz, x, y, z))
                            getRegion(x, y, z).removeFromPlayers(object);

            // Показываем обьект, но в отличие от первого случая - только для новых соседей.
            x1 = validX(region.getX() + 1);
            y0 = validY(region.getY() - 1);
            y1 = validY(region.getY() + 1);
            z0 = validZ(region.getZ() - 1);
            z1 = validZ(region.getZ() + 1);
            rx = currentRegion.getX();
            ry = currentRegion.getY();
            rz = currentRegion.getZ();
            for(int x = validX(region.getX() - 1); x <= x1; x++)
                for(int y = y0; y <= y1; y++)
                    for(int z = z0; z <= z1; z++)
                        if(!isNeighbour(rx, ry, rz, x, y, z))
                            getRegion(x, y, z).addToPlayers(object, dropper);
        }
    }

    /**
     * Удаляет обьект из текущего региона
     * @param object обьект для удаления
     */
    public static void removeVisibleObject(GameObject object)
    {
        if(object == null || object.isVisible())
            return;

        final WorldRegion currentRegion;
        if((currentRegion = object.getCurrentRegion()) == null)
            return;

        object.setCurrentRegion(null);
        currentRegion.removeObject(object);

        final int x1 = validX(currentRegion.getX() + 1);
        final int y0 = validY(currentRegion.getY() - 1);
        final int y1 = validY(currentRegion.getY() + 1);
        final int z0 = validZ(currentRegion.getZ() - 1);
        final int z1 = validZ(currentRegion.getZ() + 1);
        for(int x = validX(currentRegion.getX() - 1); x <= x1; x++)
            for(int y = y0; y <= y1; y++)
                for(int z = z0; z <= z1; z++)
                    getRegion(x, y, z).removeFromPlayers(object);
    }
Нас интересует момент когда объект добавляется в регион и удаляется с него, а именно addObject & removeObject в WorldRegion, туда мы и пойдем:
Код:
public void addObject(GameObject obj)
    {
        if(obj == null)
            return;

        lock.lock();
        try
        {
            GameObject[] objects = _objects;

            GameObject[] resizedObjects = new GameObject[_objectsCount + 1];
            System.arraycopy(objects, 0, resizedObjects, 0, _objectsCount);
            objects = resizedObjects;
            objects[_objectsCount++] = obj;

            _objects = resizedObjects;

            if(obj.isPlayer())
                if(_playersCount++ == 0)
                {
                    if(_activateTask != null)
                        _activateTask.cancel(false);
                    //активируем регион и соседние регионы через секунду
                    _activateTask = ThreadPoolManager.getInstance().schedule(new ActivateTask(true), 1000L);
                }
        }
        finally
        {
            lock.unlock();
        }
    }

    public void removeObject(GameObject obj)
    {
        if(obj == null)
            return;

        lock.lock();
        try
        {
            GameObject[] objects = _objects;

            int index = -1;

            for(int i = 0; i < _objectsCount; i++)
            {
                if(objects[i] == obj)
                {
                    index = i;
                    break;
                }
            }

            if(index == -1) //Ошибочная ситуация
                return;

            _objectsCount--;

            GameObject[] resizedObjects = new GameObject[_objectsCount];
            objects[index] = objects[_objectsCount];
            System.arraycopy(objects, 0, resizedObjects, 0, _objectsCount);

            _objects = resizedObjects;

            if(obj.isPlayer())
                if(--_playersCount == 0)
                {
                    if(_activateTask != null)
                        _activateTask.cancel(false);
                    //деактивируем регион и соседние регионы через минуту
                    _activateTask = ThreadPoolManager.getInstance().schedule(new ActivateTask(false), 60000L);
                }
        }
        finally
        {
            lock.unlock();
        }
    }
Как видно из данного кода, идет проверка на то, является ли объект игроком при add/remove из чего делаем вывод, при входе игрока в регион в пул добавляется таск на активацию региона (в данном случае через секунду) куда вошел игрок и соседних регионов.
При выходе игрока с региона в пул добавляется таск на деактивацию региона если в нем не осталось игроков, соседние региона так же будут деактивированы если в них нету игроков (в данном случае это произойдет через минуту).
Возвращаемся в World
Код:
public static void activate(WorldRegion currentRegion)
    {
        final int x1 = validX(currentRegion.getX() + 1);
        final int y0 = validY(currentRegion.getY() - 1);
        final int y1 = validY(currentRegion.getY() + 1);
        final int z0 = validZ(currentRegion.getZ() - 1);
        final int z1 = validZ(currentRegion.getZ() + 1);
        for(int x = validX(currentRegion.getX() - 1); x <= x1; x++)
            for(int y = y0; y <= y1; y++)
                for(int z = z0; z <= z1; z++)
                    getRegion(x, y, z).setActive(true);
    }

    public static void deactivate(WorldRegion currentRegion)
    {
        final int x1 = validX(currentRegion.getX() + 1);
        final int y0 = validY(currentRegion.getY() - 1);
        final int y1 = validY(currentRegion.getY() + 1);
        final int z0 = validZ(currentRegion.getZ() - 1);
        final int z1 = validZ(currentRegion.getZ() + 1);
        for(int x = validX(currentRegion.getX() - 1); x <= x1; x++)
            for(int y = y0; y <= y1; y++)
                for(int z = z0; z <= z1; z++)
                    if(isNeighborsEmpty(getRegion(x, y, z)))
                        getRegion(x, y, z).setActive(false);
    }
Идем в WorldRegion и смотрим что происходит в setActive(boolean b)
Код:
/**
     * Активация региона, включить или выключить AI всех NPC в регионе
     *
     * @param activate - переключатель
     */
    void setActive(boolean activate)
    {
        if(!_isActive.compareAndSet(!activate, activate))
            return;

        NpcInstance npc;
        for(GameObject obj : this)
        {
            if(!obj.isNpc())
                continue;
            npc = (NpcInstance) obj;
            if(npc.getAI().isActive() != isActive())
                if(isActive())
                {
                    npc.getAI().startAITask();
                    npc.getAI().setIntention(CtrlIntention.AI_INTENTION_ACTIVE);
                    npc.startRandomAnimation();
                }
                else if(!npc.getAI().isGlobalAI())
                {
                    npc.stopRandomAnimation();
                    npc.getAI().stopAITask();
                    npc.getAI().setIntention(CtrlIntention.AI_INTENTION_IDLE);
                }
        }
    }
Собственно вот и ответ, что происходит с неписями после изменения состояния региона.
Если регион стал активный, проходимся по списку всех объектов в регион и если это непись то запускаем аи, если изменился на неактивный и аишка непися не глобальная (вот кстате забыл сказать, аи неписей делятся на глобальные и не глобальные, глобальные запускаются сразу при спауне непися и работаю вне зависимости от того активен регион или нет, это кстате видно так же тут в коде).
Ну как-то так, а в вообще можно еще дальше копнуть в DefaultAI и посмотреть в startAITask и stopAITask
 
Последнее редактирование:

Psycho

Я пчела. Бжж-жж...
Легенда
#7
Если выставить минимум настроек графы (выше хф хз), то видно как анимация у мобов и нпц начинает хромать. Мне кажется, что есть определенная прогрузка, и вряд ли это зависит от того, на чем написан сервер. Видимо это дерьмо в самом движке/клиенте. Т.к. есть опция отображения дальности прорисовки, и отображения объектов/нпц.
 

Rozhek

Пляшущий с бубном
Местный
#8
Пришел поздно, попробую дополнить то, что не описывали.
На ПТС спавном мобов управляют так называемые "мейкеры": к каждому мейкеру привязаны 1 или более выделенной территории. Соответственно монстры привязаны к этой территории, а территория к координатам. И работа АИ зачастую отталкивается как раз от входа\выхода игрока в эту территорию. А вот для пакетной обработки и выставления флага активности нпц используются отдельно сформированные области мира, а не эти территории.
Моб видит игрока только по радиусу входа в область взаимодействия, но сам способ взаимодействия описан в скриптах каждого нпц, а не ядре(атаковать\ничего не делать\делать что то еще). При этом на С4 использовалась только одна конструкция - очередь действий: действие, вес действия, объект взаимодействия(игрок, другой нпц или нпц сам по себе). В зависимости от того, как с нпц повзаимодействовали(атаковали\вошли в радиус видения\ вышли из территории) алгоритм АИ определяет тип и вес задачи и добавляет в список. При отсутствии задачи, выбирается задача с наибольшим весом и выполняется.
Начиная с каких-то хроник С6 или чуть позже(не искал точку невозврата) корейцы добавили новый тип АИ - условно "маги". И вдобавок к очереди задач ввели список хейта моба. Теперь у этого типа мобов при взаимодействии с игроком дополнительно добавляется информация об обидчике и величина ненависти. И есть АИ где этих хейт списков несколько и каждый заполняется отдельным набором игроков. Алгоритм встраивает обработку этого\этих списков хейта и добавляет задачи с учетом их содержания, также как и удаляет\очищает список хейта по определенным условиям в моменты выполнения задач. Обобщенно, это позволило мобам-магам генерировать задачи каста исходя из списка хейта, а не создавать по одной задаче каста, на каждый удар игрока.
 

Howli

Участник
Пользователь
#9
видно как анимация у мобов и нпц начинает хромать
это кривизна клиента.
В клиенте можно найти такие жуткие косяки и костыли, видно древние времена, и лепили видимо как могли и похоже даже разные разрабы.
Пример, переделывая модели, встречаю необьяснимую кривизну, смотрим скелет:
bip01
+-Bip01_Pelvis
+-bip01_spine
+--Bip01_Spine1
+---bip01_spine2
+----Bip01_Neck
Вроде обычный скелет человека, но куда повернут вектор Bip01_Spine1 - вообще хз куда, и от него перпендикулярно Bip01_Spine1 - бред же. Потом смотрим таз, оказывается низ таза Bip01_Pelvis весами связан с bip01_spine - бред полный. В итоге как разрабы выкрутились - вектор bip01_spine во всех анимациях имеет ориентацию 1, что не влияет ни на что. Для меня загадка - зачем было городить этот кастыль, если можно было просто повернуть правильно кость спины и не связывать ее с тазом.
Потом когда доделывал анимацию - идем по фреймам, например, воина файтера - bip01 всегда находится в координатах 0-фрейма, идет изменение ориентации других костей - чем выполняется походка человека.
Теперь смотрим темную ельфийку - шо за фигня тут творится - bip01 скачется в каждой фрейме по позиции чтоб визуально не было видно не поподанение ориентации других костей на общую сцену. Это что лепил уже другой разраб? И лепил так чтоб приняли визуально, и пофигу что внутри творится.
Смотрим Камаеля - прям красотище, повторные фреймы сжаты, позиция не пляшет, четко подогнаны фреймы, просто минимум затрат на выполнение тех же действий. Видимо их делал уже опытный разраб набивший руку.
 

lordofdest

Интересующийся
Партнер
#10
Ахренеть, почти так как я придумал, я думал поднять поток каждому квадрату, который проверял бы наличие в нем игроков, если игроков несколько, то высчитывался бы минимальные и максимальные координаты радиуса игроков, добавлять эти координаты в массив текущего квадрата(и соседних, если радиус их затрагивает), и уже поток квадрата относительно координатам радиуса рисовал бы мобов, раз в какое то время проверял бы видимых мобов, нет ли у них желания пройтись или разовнятся, а уже заагреному мобу поднимать отдельный поток(или всех заагреных мобов в радиусе обрабатывать одним потоком , не решил)
Не думаю что хорошая идея предоставлять каждому квадрату по потоку.
Если брать тот же ПТС то там стандартный пул на обработку мира в 4 потока, но может расширятся до количества ядер.
 

xDarkDelux

Бывалый
Проверенный
#11
это кривизна клиента.
В клиенте можно найти такие жуткие косяки и костыли, видно древние времена, и лепили видимо как могли и похоже даже разные разрабы.
Пример, переделывая модели, встречаю необьяснимую кривизну, смотрим скелет:
bip01
+-Bip01_Pelvis
+-bip01_spine
+--Bip01_Spine1
+---bip01_spine2
+----Bip01_Neck
Вроде обычный скелет человека, но куда повернут вектор Bip01_Spine1 - вообще хз куда, и от него перпендикулярно Bip01_Spine1 - бред же. Потом смотрим таз, оказывается низ таза Bip01_Pelvis весами связан с bip01_spine - бред полный. В итоге как разрабы выкрутились - вектор bip01_spine во всех анимациях имеет ориентацию 1, что не влияет ни на что. Для меня загадка - зачем было городить этот кастыль, если можно было просто повернуть правильно кость спины и не связывать ее с тазом.
Потом когда доделывал анимацию - идем по фреймам, например, воина файтера - bip01 всегда находится в координатах 0-фрейма, идет изменение ориентации других костей - чем выполняется походка человека.
Теперь смотрим темную ельфийку - шо за фигня тут творится - bip01 скачется в каждой фрейме по позиции чтоб визуально не было видно не поподанение ориентации других костей на общую сцену. Это что лепил уже другой разраб? И лепил так чтоб приняли визуально, и пофигу что внутри творится.
Смотрим Камаеля - прям красотище, повторные фреймы сжаты, позиция не пляшет, четко подогнаны фреймы, просто минимум затрат на выполнение тех же действий. Видимо их делал уже опытный разраб набивший руку.
Как мне сказал один человек: "У них энтерпрайз, им нужно не шибко большим коллективом за не шибко большие бабки сделать ТЗ. и они его делают."
 

DaVilka

Интересующийся
Местный
#12
Не думаю что хорошая идея предоставлять каждому квадрату по потоку.
Если брать тот же ПТС то там стандартный пул на обработку мира в 4 потока, но может расширятся до количества ядер.
Имел ввиду, каждому квадрату, в котором находятся люди, у меня нету опыта в этой области, но если 4 потока способны обрабатывать онлайн больше 1к, то по потоку на квадрат действительно тупо выделять, думаю из сурсом это можно еще отконтролировать, аля "нахрена поднимать 4 потока, если на серве 10 человек"
 

DaVilka

Интересующийся
Местный
#14
Интересное у нас тут комьюнити, задаёшь простой вроде бы вопрос, а на тебя вываливается экспертное мнение доброй половины форума :pandaredlol:
Дак это же хорошо :-) Чем больше информации по теме, тем лучше, особенно когда не охота разбираться в сурсах )
 

Rozhek

Пляшущий с бубном
Местный
#15
Имел ввиду, каждому квадрату, в котором находятся люди, у меня нету опыта в этой области, но если 4 потока способны обрабатывать онлайн больше 1к, то по потоку на квадрат действительно тупо выделять, думаю из сурсом это можно еще отконтролировать, аля "нахрена поднимать 4 потока, если на серве 10 человек"
Насчёт потоков кста, высокая производительность достигается же не количеством потоков, а их распределённой нагрузкой. Основной сервер и все основные менеджеры/управляющие механизмы крутятся на основном потоке. А остальные потоки ты нагружаешь по мере возможности/необходимости.
Мобов в мире много, все они что то делают, поэтому их обработчики и раскидывают на разные процы/потоки каким то способом распределения.
На ява серверах в многопоточность кроме аи, например, ещё системы событий правильно отправлять, которые потом подхватываются аишками с выделенным тиком. Событий тоже много и это тоже сильно разгружает основной поток.
 

Visor123

L2Emu Enterprise
Партнер
#16
В мире много мобов и нпц, как они живут со стороны сервера? :-) Не создается же отдельный поток для каждого моба и нпц. По какой технологии они двигаются?
Java. Мир по квадратам.
1) Активируются квадраты.
2) у мобов/РБ включаются АИ. АИ постоянно пускает задачу активности моба. Это отдельный поток.
Потоки обрабатываются параллельно, сама активность задается в пуле.
Двигаются мобы, если активность по рандому пошло на движение в рандомную точку в пределах радиуса или зоны спавна.
Потоки обработки АИ конечны. Класс АИ - это просто класс, а класс implements Runnable запускается как отдельный поток и уже действует в соответствии с кодом.
В общих понятиях вроде все.
 
Сверху Снизу