Як ігрові об’єкти повинні усвідомлювати один одного?


18

Мені важко знайти спосіб впорядкувати ігрові об’єкти так, щоб вони були поліморфними, але в той же час не поліморфними.

Ось приклад: припускаючи, що ми хочемо, щоб всі наші об’єкти були до update()та draw(). Для цього нам потрібно визначити базовий клас, у GameObjectякого є ці два віртуальні чисті методи, і поліморфізм несе:

class World {
private:
    std::vector<GameObject*> objects;
public:
    // ...
    update() {
        for (auto& o : objects) o->update();
        for (auto& o : objects) o->draw(window);
    }
};

Метод оновлення повинен дбати про те, в якому стані потребує оновлення конкретного класу. Справа в тому, що кожен об’єкт повинен знати про навколишній світ. Наприклад:

  • Шахта повинна знати, чи хтось зіткнувся з нею
  • Солдат повинен знати, чи знаходиться солдат іншої команди в близькості
  • Зомбі повинен знати, де знаходиться найближчий мозок, в радіусі

Для пасивних взаємодій (як і першої) я думав, що виявлення зіткнення може делегувати, що робити в конкретних випадках зіткнення, з самим об'єктом a on_collide(GameObject*).

Більшість інших відомостей (як і інші два приклади) можна було просто запитати по ігровому світу, переданому updateметоду. Зараз світ не розрізняє об’єкти за їх типом (він зберігає весь об'єкт в одному поліморфному контейнері), тому те, що насправді він поверне з ідеалом, world.entities_in(center, radius)є контейнером GameObject*. Але звичайно солдат не хоче нападати на інших солдатів зі своєї команди, і зомбі не стосується інших зомбі. Тому нам потрібно відрізняти поведінку. Рішенням може бути наступне:

void TeamASoldier::update(const World& world) {
    auto list = world.entities_in(position, eye_sight);
    for (const auto& e : list)
        if (auto enemy = dynamic_cast<TeamBSoldier*>(e))
            // shoot towards enemy
}

void Zombie::update(const World& world) {
    auto list = world.entities_in(position, eye_sight);
    for (const auto& e : list)
        if (auto enemy = dynamic_cast<Human*>(e))
            // go and eat brain
}

але, звичайно, кількість dynamic_cast<>кадрів може бути жахливо великою, і всі ми знаємо, наскільки dynamic_castможе бути повільним . Ця ж проблема стосується і on_collide(GameObject*)делегата, про який ми говорили раніше.

То який ідеальний спосіб організувати код, щоб об’єкти могли знати про інші об’єкти та мати можливість їх ігнорувати чи вживати дій залежно від їх типу?


1
Я думаю, що ви шукаєте універсальну C ++ RTTI спеціальну реалізацію. Тим не менш, ваше питання, здається, не стосується лише розумних механізмів RTTI. Те, що ви вимагаєте, вимагається майже будь-яким проміжним програмним забезпеченням, яке буде використовувати гра (система анімації, фізика, щоб назвати декілька). Залежно від списку підтримуваних запитів, ви можете обдурити свій шлях навколо RTTI, використовуючи ідентифікатори та індекси в масивах, або в кінцевому підсумку спроектуєте повноцінний протокол для підтримки більш дешевих альтернатив для динамічного_cast та type_info.
теодрон

Я б радив не використовувати систему типу для логіки гри. Наприклад, замість того, щоб залежати від результату dynamic_cast<Human*>, реалізуйте щось на зразок a bool GameObject::IsHuman(), яке повертається falseза замовчуванням, але перевизначається для повернення trueв Humanклас.
congusbongus

додатково: ви майже ніколи не надсилаєте тону предметів один одному, що може зацікавити їх. Це очевидна оптимізація, яку вам доведеться дійсно врахувати.
теодрон

@congusbongus Використання vtable та користувальницьких IsAперекриттів виявилося лише незначно кращими, ніж динамічний кастинг на практиці для мене. Найкраще зробити, щоб користувач міг, де це можливо, сортувати списки даних, а не сліпо повторювати весь пул сутностей.
теодрон

4
@Jefffrey: в ідеалі ви не пишете специфічний код. Ви пишете інтерфейс -специфічний код ("інтерфейс" у загальному розумінні). Ваша логіка для a TeamASoldierі TeamBSoldierсправді однакова - стріляйте в когось з іншої команди. Все, що потрібно іншим об'єктам, - це GetTeam()метод, який є найбільш конкретним, і, на прикладі конгусбунгуса, який можна ще більше абстрагувати у IsEnemyOf(this)вигляді інтерфейсу. Код не повинен стосуватися таксономічних класифікацій солдатів, зомбі, гравців тощо. Зосередьтеся на взаємодії, а не на типах.
Шон Міддлічч

Відповіді:


11

Замість того, щоб реалізовувати рішення кожного суб’єкта самостійно, ви можете альтернативно використовувати схему контролера. У вас є центральні класи контролерів, які знають про всі об'єкти (що їм важливо) і контролюють їх поведінку.

MovementController буде обробляти переміщення всіх об'єктів, які можуть рухатися (робити пошук маршруту, оновлювати позиції на основі поточних векторів руху).

Контролер MineBehaviorControl перевірив усі міни та всіх солдатів і наказав міні вибухнути, коли солдат стане занадто близько.

Контролер ZombieBehaviorController перевіряв би всіх зомбі та солдатів, що знаходяться в їх околицях, вибирав найкращу ціль для кожного зомбі та наказував йому переміщатися туди і атакувати його (самим ходом керує MovementController).

SoldierBehaviorController проаналізував би всю ситуацію, а потім створив би тактичні вказівки для всіх солдатів (ти переїжджаєш туди, ти це стріляєш, ти зцілюєш цього хлопця ...). Фактичне виконання цих команд вищого рівня також оброблялося б контролерами нижчого рівня. Доклавши до цього певних зусиль, ви могли б зробити AI здатним до досить розумних спільних рішень.


1
Ймовірно, це також відома як "система", яка управляє логікою для певних типів компонентів в архітектурі Entity-Component.
теодрон

Це звучить як рішення у стилі С. Компоненти згруповані в std::maps і сутності - це лише ідентифікатори, і тоді нам доведеться скласти якусь систему типу (можливо, з компонентом тегів, тому що рендері буде потрібно знати, що малювати); і якщо ми не хочемо цього робити, нам знадобиться скласти малюнок: але йому потрібен компонент позиції, щоб знати, куди звернутись, тому ми створюємо залежності між компонентами, які ми вирішуємо за допомогою надскладної системи обміну повідомленнями. Це те, що ви пропонуєте?
взуття

1
@Jefffrey "Це звучить як рішення у стилі С" - навіть коли це було б правдою, чому це обов'язково було б поганим? Інші проблеми можуть бути справедливими, але для них є рішення. На жаль, коментар є занадто коротким, щоб правильно їх вирішити.
Філіпп

1
@Jefffrey Використання підходу, коли самі компоненти не мають логіки, а "системи" відповідають за обробку всієї логіки, не створюють залежностей між компонентами, а також не вимагає надскладної системи обміну повідомленнями (принаймні, не настільки складною) . Дивіться наприклад: gamadu.com/artemis/tutorial.html

1

Перш за все, спробуйте реалізувати функції, щоб об'єкти залишалися незалежними один від одного, коли це можливо. Особливо ви хочете зробити це для багатопотокової різьби. У вашому першому прикладі коду набір усіх об'єктів можна розділити на набори, що відповідають кількості ядер процесора, та оновити їх дуже ефективно.

Але, як ви вже говорили, для деяких особливостей потрібна взаємодія з іншими об’єктами. Це означає, що стан усіх об'єктів повинен бути синхронізований в деяких точках. Іншими словами, ваша програма повинна дочекатися спочатку завершення всіх паралельних завдань, а потім застосувати обчислення, що передбачають взаємодію. Добре зменшити кількість цих точок синхронізації, оскільки вони завжди означають, що одні потоки повинні чекати, коли інші закінчать.

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

Отже, те, що ви робите один раз у кадрі, - це 1. буфер поточного стану об'єктів у всьому світі, 2. оновлення всіх об'єктів на основі себе та буфера, 3. намалювання об'єктів і потім починайте з оновлення буфера.


1

Використовуйте систему на основі компонентів, в якій у вас є безпрограшний GameObject, який містить 1 або більше компонентів, які визначають їх поведінку.

Наприклад, скажімо, що якийсь об'єкт повинен рухатися вліво і вправо (платформа), ви можете створити такий компонент і приєднати його до GameObject.

Тепер скажімо, що ігровий об’єкт повинен повільно обертатися весь час, ви можете створити окремий компонент, який робить саме це, і приєднати його до GameObject.

Що робити, якщо ви хотіли мати рухому платформу, яка також обертається, в традиційній ієрархії класу, що стає важко зробити без дублювання коду.

Краса цієї системи полягає в тому, що замість класу Rotatable або MovingPlatform ви приєднуєте обидва ці компоненти до GameObject, і тепер у вас є MovingPlatform, який AutoRotates.

Всі компоненти мають властивість, 'RequUpdate', яка, хоча істина, GameObject буде називати метод 'update' на зазначеному компоненті. Наприклад, скажімо, що у вас є компонент, який перетягується, цей компонент при знищенні миші (якщо він був над GameObject) може встановити значення "requUpdate" на істинне, а потім при встановленні мишею встановити значення false. Дозволити йому слідувати за мишкою лише тоді, коли миша вниз.

Один з розробників Tony Hawk Pro Skater написав на ньому дефакто, і це варто прочитати: http://cowboyprogramming.com/2007/01/05/evolve-your-heirachy/


1

Улюблений склад над спадщиною.

Моя найсильніша порада, окрім цього, була б такою: не зациклюйтеся на думці "я хочу, щоб це було надзвичайно гнучким". Гнучкість велика, але пам’ятайте, що на якомусь рівні в будь-якій кінцевій системі, наприклад, в грі, є атомні частини, які використовуються для побудови цілого. Так чи інакше, ваша обробка покладається на ті заздалегідь визначені, атомні типи. Іншими словами, обслуговування "будь-якого" типу даних (якщо це можливо) не допоможе вам у довгостроковій перспективі, якщо у вас немає коду для їх обробки. По суті, весь код повинен аналізувати / обробляти дані на основі відомих специфікацій ... що означає попередньо визначений набір типів. Наскільки великий цей набір? До вас.

Ця стаття пропонується зрозуміти принцип складання над успадкуванням в розробці ігор за допомогою надійної та ефективної архітектури сукупності компонентів.

Створюючи сутності з (різних) підмножин деякого надмножини заздалегідь визначених компонентів, ви пропонуєте своїм ШІ конкретні, детальні способи осмислення світу та акторів навколо них, читаючи стани цих компонентів акторів.


1

Особисто я рекомендую зберігати функцію малювання поза самим класом Object. Я навіть рекомендую тримати розташування / координати Об'єктів поза самим Об'єктом.

Цей метод draw () має справу з API рендерингу низького рівня або OpenGL, OpenGL ES, Direct3D, і вашим шаром обгортання для цих API, або API двигунів. Можливо, вам доведеться поміняти місцями між собою (Якщо ви хочете підтримати OpenGL + OpenGL ES + Direct3D, наприклад.

Цей GameObject повинен просто містити основну інформацію про його візуальний зовнішній вигляд, такий як Mesh або, можливо, більший набір, включаючи шейдерні входи, стан анімації тощо.

Крім того, ви хочете отримати гнучкий графічний конвеєр. Що станеться, якщо ви хочете замовити об’єкти залежно від їх відстані до камери. Або їх тип матеріалу. Що станеться, якщо ви хочете намалювати 'вибраний' об'єкт іншого кольору. Що робити, якщо замість того, щоб насправді відтворити так soo, як ви викликаєте функцію малювання на об’єкті, він замість цього вводить його в список команд дій для виконання візуалізації (може знадобитися для нанизування). Ви можете робити таку річ за допомогою іншої системи, але це PITA.

Що я рекомендую, замість того, щоб малювати безпосередньо, ви прив'язуєте всі об'єкти, які ви хочете, до іншої структури даних. Для цього прив'язки дійсно потрібно мати посилання на розташування об'єктів та інформацію про надання.

Ваші рівні / шматки / райони / карти / хаби / весь світ / що завгодно отримують просторовий індекс, він містить об'єкти та повертає їх на основі запитів координат і може бути простим списком чи чимось на зразок Окрем. Це також може бути обгорткою чогось, що реалізується стороннім фізичним двигуном як сцена фізики. Це дозволяє вам робити такі речі, як "Запитати всі об'єкти, які перебувають у зорі камери з деякою додатковою зоною навколо них", або для більш простих ігор, де ви можете просто зробити все, що захоплює весь список.

Просторові покажчики не повинні містити фактичної інформації про позиціонування. Вони працюють, зберігаючи предмети в структурах дерев по відношенню до місця розташування інших об’єктів. Вони можуть бути хоч як своєрідний кеш-програш, що дозволяє швидко шукати об'єкт, виходячи з його положення. Немає реальної необхідності дублювати фактичні координати X, Y, Z. Сказавши, що ви можете, якщо хочете зберегти

Насправді ваші ігрові об’єкти навіть не повинні містити власну інформацію про місцезнаходження. Наприклад, об'єкт, який не був поставлений на рівень, не повинен мати координат x, y, z, що не має сенсу. Ви можете містити це у спеціальному покажчику. Якщо вам потрібно шукати координати об'єкта на основі його фактичного посилання, тоді вам потрібно буде встановити зв'язок між об'єктом та графіком сцени (графіки сцен призначені для повернення об'єктів на основі координат, але повільні при поверненні координат на основі об'єктів) .

Коли ви додаєте об'єкт до рівня. Він зробить наступне:

1) Створіть структуру місцеположення:

 class Location { 
     float x, y, z; // Or a special Coordinates class, or a vec3 or whatever.
     SpacialIndex& spacialIndex; // Note this could be the area/level/map/whatever here
 };

Це також може бути посиланням на об'єкт у сторонніх фізичних двигунах. Або це можуть бути координати зміщення з посиланням на інше місце (для камери відстеження, прикріпленого об'єкта чи прикладу). З поліморфізмом це може бути або залежно від того, чи є статичним чи динамічним об'єктом. Зберігаючи посилання на просторовий індекс тут, коли координати оновлюються, просторовий індекс також може бути.

Якщо вас турбує динамічне розподіл пам’яті, використовуйте пул пам’яті.

2) Прив'язка / зв'язок між вашим об'єктом, його місцезнаходженням та графіком сцени.

typedef std::pair<Object, Location> SpacialBinding.

3) Прив'язка додається до просторового індексу всередині рівня у відповідній точці.

Коли ви готуєтеся до надання.

1) Дістаньте камеру (це буде просто інший об'єкт, за винятком того, що місце розташування буде відстежувати характер гравців, і ваш рендер матиме спеціальну посилання на нього, адже це все, що йому дійсно потрібно).

2) Отримайте SpacialBinding камери.

3) Отримайте просторовий індекс від зв’язування.

4) Запитайте об’єкти, які (можливо) видимі для камери.

5А) Вам потрібно обробляти візуальну інформацію. Текстури, завантажені в GPU тощо. Це найкраще зробити заздалегідь (наприклад, на рівні навантаження), але, можливо, це можна зробити під час виконання (для відкритого світу ви можете завантажувати речі, коли ви наближаєтесь до шматка, але це все одно слід робити заздалегідь).

5B) Необов’язково побудуйте кешоване дерево візуалізації, якщо ви хочете впорядкувати глибину / матеріал або відслідковувати об'єкти, які знаходяться поблизу, вони можуть бути видно пізніше. Інакше ви можете просто запитувати просторовий індекс кожного разу, коли це залежатиме від ваших вимог до гри / продуктивності.

Ваш візуалізатор, ймовірно, знадобиться об'єкт RenderBinding, який буде зв'язувати між Об'єктом, координатами

class RenderBinding {
    Object& object;
    RenderInformation& renderInfo;
    Location& location // This could just be a coordinates class.
}

Потім, коли ви надаєте, просто запустіть список.

Я використовував посилання вище, але вони можуть бути розумними вказівниками, необробленими вказівниками, ручками об'єктів тощо.

Редагувати:

class Game {
    weak_ptr<Camera> camera;
    Level level1;

    void init() {
        Camera camera(75.0_deg, 1.025_ratio, 1000_meters);
        auto template_player = loadObject("Player.json")
        auto player = level1.addObject(move(player), Position(1.0, 2.0, 3.0));
        level1.addObject(move(camera), getRelativePosition(player));

        auto template_bad_guy = loadObject("BadGuy.json")
        level1.addObject(template_bad_guy, {10, 10, 20});
        level1.addObject(template_bad_guy, {10, 30, 20});
        level1.addObject(move(template_bad_guy), {50, 30, 20});
    }

    void render() {
        camera->getFrustrum();
        auto level = camera->getLocation()->getLevel();
        auto object = level.getVisible(camera);
        for(object : objects) {
            render(objects);
        }
    }

    void render(Object& object) {
        auto ri = object.getRenderInfo();
        renderVBO(ri.getVBO());
    }

    Object loadObject(string file) {
        Object object;
        // Load file from disk and set the properties
        // Upload mesh data, textures to GPU. Load shaders whatever.
        object.setHitPoints(// values from file);
        object.setRenderInfo(// data from 3D api);
    }
}

class Level {
    Octree octree;
    vector<ObjectPtr> objects;
    // NOTE: If your level is mesh based there might also be a BSP here. Or a hightmap for an openworld
    // There could also be a physics scene here.
    ObjectPtr addObject(Object&& object, Position& pos) {
        Location location(pos, level, object);
        objects.emplace_back(object);
        object->setLocation(location)
        return octree.addObject(location);
    }
    vector<Object> getVisible(Camera& camera) {
        auto f = camera.getFtrustrum();
        return octree.getObjectsInFrustrum(f);
    }
    void updatePosition(LocationPtr l) {
        octree->updatePosition(l);
    }
}

class Octree {
    OctreeNode root_node;
    ObjectPtr add(Location&& object) {
        return root_node.add(location);
    }
    vector<ObjectPtr> getObjectsInRadius(const vec3& position, const float& radius) { // pass to root_node };
    vector<ObjectPtr> getObjectsinFrustrum(const FrustrumShape frustrum;) {//...}
    void updatePosition(LocationPtr* l) {
        // Walk up from l.octree_node until you reach the new place
        // Check if objects are colliding
        // l.object.CollidedWith(other)
    }
}

class Object {
    Location location;
    RenderInfo render_info;
    Properties object_props;
    Position getPosition() { return getLocation().position; }
    Location getLocation() { return location; }
    void collidedWith(ObjectPtr other) {
        // if other.isPickup() && object.needs(other.pickupType()) pick it up, play sound whatever
    }
}

class Location {
    Position position;
    LevelPtr level;
    ObjectPtr object;
    OctreeNote octree_node;
    setPosition(Position position) {
        position = position;
        level.updatePosition(this);
    }
}

class Position {
    vec3 coordinates;
    vec3 rotation;
}

class RenderInfo {
    AnimationState anim;
}
class RenderInfo_OpenGL : public RenderInfo {
    GLuint vbo_object;
    GLuint texture_object;
    GLuint shader_object;
}

class Camera: public Object {
    Degrees fov;
    Ratio aspect;
    Meters draw_distance;
    Frustrum getFrustrum() {
        // Use above to make a skewed frustum box
    }
}

Що стосується того, щоб робити речі «обізнаними» один про одного. Це виявлення зіткнення. Це, мабуть, було б реалізовано в Указі. Вам потрібно буде надати деякий зворотний дзвінок у вашому головному об’єкті. З цими речами найкраще обробляти належний фізичний двигун, такий як Куля. У цьому випадку просто замініть Octree на PhysicsScene та Position на посилання на щось на зразок CollisionMesh.getPosition ().


Ого, це виглядає дуже добре. Я думаю, що я зрозумів основну ідею, але без іншого прикладу я не можу повністю зрозуміти зовнішній вигляд цього. Чи є у вас більше посилань або живих прикладів на це? (Тим часом я продовжую читати цю відповідь).
Взуття

Насправді немає прикладів, це якраз те, що я планую зробити, коли знайду час. Я додам ще кілька загальних занять і побачу, чи це допомагає. Є це і це . мова йде більше про об’єктні класи, ніж про те, як вони співвідносяться або рендерінг. Оскільки я сам цього не реалізував, можуть виникнути підводні камені, біти, які потребують розробки або продуктивності, але я думаю, що загальна структура в порядку.
Давид К. Єпископ
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.