WarZes


СтатьиФорумИнфо

Блог

25 окт 2013

Потихоньку завершаю работу с низкоуровневым рендером и пора подумать о организации сцены. Первоначально я горел идеей компонентной системы, но поработав в юнити в ней разочаровался. Идея компонентной системы в том что любой объект может состоять из разного числа компонентов. Но тут возникает первая проблема - идентификация этих компонент. Как узнать что вот этот компонент - колайдер, а вон тот - рендер меш? Вообще в идеальном вакууме, я думаю, компонентная система и не подразумевала что кто-то будет пытаться опознавать компоненты. Но в реальности это выглядит как-то так (С#)

Collider col = gameObject.GetComponent<Collider>();

или так

Collider col = gameObject.GetComponent("Collider");

Второй вариант ближе к C++... Но искать по имени (да даже если по хешу)... А если одинаковых компонентов несколько? Вообщем это не то что хорошо для реалтайма.

После этого я заметил еще одно свойство с которым столкнулся в юнити. Хоть по идее можно на объект вешать несколько компонентов, но есть уникальные, которые могут быть только в единственном числе:
Transform - этот компонент вообще должен быть у всех объектов (тут кстати уже обсуждали  ранее эту проблему)
Collider - в юнити невозможно добавить два колайдера (попытка через редактор удалит старый, попытка через код приведет к error)
MeshRender - их вроде как можно несколько, но реально ли это вам нужно? Так что тоже всегда по одному на объект
Остальные компоненты - в юнити есть много компонент которые существуют в единственном числе.

А чего же тогда много? Скриптовых компонент - их может быть куча на каждом объекте.

И тут я подхожу к финальной мысли - к той системе, которую я хочу получить.

Есть GameObject, и у него есть свойства

class GameObject
{
     Transform
     Collider*
     MeshRender*

     list<Script*>
};

У каждого GO есть свойство Transform которое определяет его расположение в мире
Еще у них есть несколько указателей на свойства (Collider, MeshRender). они могут быть нулевыми - а значит у данного объекта их нет, то есть не все GO нуждаются в колайдере, и даже есть возможность создать GO которые не будут рисоваться на экран. Свойств этих будет много. Но любой GO не обязан содержать их все. Сейчас вы спросите - а как же возможность добавлять новые свойства? А вот приведите мне такие свойства которые пользователь захочет добавить к ГО, а то у меня идей нет:)
И в конце есть список скриптов (возможно так будут сделаны и еще несколько вещей, например материалы). То есть их может быть много, и именно ими пользователь будет задавать остальные нужные ему свойства.

Таким образом я решу проблему поиска компонентов. Если мне надо будет колайдер, то я напишу так:

Collider *col = gameObject->GetCollider();
if (col)
{
}

Кстати, в юнити это тоже есть - у GO есть несколько сохраненных компонент которые можно использовать примерно также:

animation  The Animation attached to this GameObject (Read Only). (null if there is none attached).
audio  The AudioSource attached to this GameObject (Read Only). (null if there is none attached).
camera  The Camera attached to this GameObject (Read Only). (null if there is none attached).
collider  The Collider attached to this GameObject (Read Only). (null if there is none attached).
constantForce  The ConstantForce attached to this GameObject (Read Only). (null if there is none attached).
guiText  The GUIText attached to this GameObject (Read Only). (null if there is none attached).
guiTexture  The GUITexture attached to this GameObject (Read Only). (null if there is none attached).
hingeJoint  The HingeJoint attached to this GameObject (Read Only). (null if there is none attached).
light  The Light attached to this GameObject (Read Only). (null if there is none attached).
networkView  The NetworkView attached to this GameObject (Read Only). (null if there is none attached).
particleEmitter  The ParticleEmitter attached to this GameObject (Read Only). (null if there is none attached).
particleSystem  The ParticleSystem attached to this GameObject (Read Only). (null if there is none attached).
renderer  The Renderer attached to this GameObject (Read Only). (null if there is none attached).
rigidbody  The Rigidbody attached to this GameObject (Read Only). (null if there is none attached).
transform  The Transform attached to this GameObject. (null if there is none attached).

Как видите, они тоже могут быть нулевыми как у меня:) Но у меня это будет более явно

Вообщем я просто взял лучшее (на мой взгляд) из компонентной системы и классической системы энтити и смешал

Это по GameObject.

Теперь про сцену
У меня будет World, это класс полностью хранящий все происходящее. Он будет содержать сцены (Scene). Каждая сцена равна сцене из юнити. В любой момент времени может быть только одна сцена... Но их можно смешивать (то есть из двух сцен создать одну, вообщем тоже идея из юнити). На сцене может быть любое число ГО. Также на сцене есть одна камера, потому что я не понимаю этой фичи в юнити с возможностью создать сцену без камеры (и получить черный экран) или создать несколько го с камерами. так что у меня основная камера будет всегда.
Го могут строится иерархию по принципу родитель - ребенок как в юнити, потому что это мне тоже понравилось. Корневого Го не будет. Каждый Го является корнем, и может содержать детей, но может и не содержать.

Ссылка | Комментарии [60]

16 сен 2013

Гы, в связи с тем что меня вообщем тоже устраивает unity3d, и в ближайшее время я собираюсь проекты пилить на нем, а еще потому что от сапфира я устал, я решил начать новый движок

Но теперь не будет никаких глупых целей и самообмана. Мне просто нравится писать движки, поэтому я буду писать движок в свое удовольствие, только ради движка и самого процесса.

Новый движок я назвал аметистом (Amethyst Engine):) хотел назвать ониксом, но это имя занято юбисофтом.

В отличие от сапфира, я буду теперь более открыто вести разработку, то есть описывать весь процесс (но кратко, ибо время) создания от самого начала. для чего? just for lulz, конечно же. Ну и еще потому что мне в свое время такого не хватало, по началу тупо не понятно с чего начать

Уже есть де записи
http://zeswarchief.blogspot.ru/2013/09/amethyst-engine.html
http://zeswarchief.blogspot.ru/2013/09/amethyst-engine-02.html

Остальные в коментариях

Вообще думал сюда их постить, но если я буду так писать каждый день здесь, меня чего доброго еще забанят, ибо полезной инфы не так уж и много:)
Лучше я буду аппать эту тему, как делал с уроками по DX 11

p.s. если вы спросите что будет теперь с уроками DX 11, то отвечу, наверное пока ничего, так как основную часть я уже рассмотрел, там пошли более сложные темы, для которых у меня тупо не хватает теоретических знаний чтобы толково объяснять, так что вернусь к ним когда чуток еще прокачаюсь в теории.

p.s.2 это не значит что я все пишу с нуля, Core и Math а также еще кой-какая часть кода будут взяты из сапфира

Ссылка | Комментарии [60]

10 июня 2013

Сидел я тут, размышлял... Недавно в моем движке объявился OGL (теперь у меня два GAPI - DX11 и OGL). Сделал я классически, через виртуальные функции... Но я любитель глупых оптимизаций, поэтому захотелось избавиться от виртуальных функций. Но и делать две разные версии движка не охота, слишком долго собирается. И тут мне пришло простое решение - сделать три вида сборки - GAPI специфичное и мультирендер. Мультирендер позволит на лету менять DX 11 на OGL. Но ведь это не везде а точнее нигде не нужно. Поэтому движок можно будет собрать под отдельный GAPI.

В примере три дефайна - MULTI (мультирендер), OGL и DX11.

Render.h

#ifdef MULTI
class Render
#else
template<class T>
class TRender
#endif
{
public:
#ifdef MULTI
  virtual void Run() = 0;
#else
  void Run()
  {
    m_render->Run();
  }
#endif
private:
#ifndef MULTI
    T *m_render;
#endif
};

#ifdef DX11
typedef TRender<DX11> Render;
#elif OGL
typedef TRender<OGL> Render;
#endif

DX11.h

#ifdef MULTI
class MyFoo : public Foo
#else
class MyFoo
#endif
{
public:
  void Run();
}

Один минус - код страшненький.

Класс Render можно переписать и по другому:

#ifdef MULTI
class Render
#elif DX11
class Render : public DX11
#elif OGL
class Render : public OGL
#endif
{
public:
#ifdef MULTI
  virtual void Run() = 0;
#endif
};

Так наверное даже лучше, нет лишних функций и экономия на целых 4 байта (sic), хотя теперь рендер должен знать о своим потомках (но с другой стороны, сколько их там кроме OGL и DX?). да и не совсем привычно (в одной сборке DX11 и OGL потомки Render, в другой Render потомок DX11 или OGL)

Пишу здесь, так как раньше не смог до такого додуматься:) И как раз проверю, влияют ли виртуальные методы на хоть что-то.

Ссылка | Комментарии [51]

11 мая 2013

Factory Method (Фабричный метод)


Из вики:

Фабричный метод (англ. Factory Method) — порождающий шаблон проектирования, предоставляющий подклассам интерфейс для создания экземпляров некоторого класса. В момент создания наследники могут определить, какой класс создавать. Иными словами, Фабрика делегирует создание объектов наследникам родительского класса. Это позволяет использовать в коде программы не специфические классы, а манипулировать абстрактными объектами на более высоком уровне. Также известен под названием виртуальный конструктор (англ. Virtual Constructor).

Так вот он какой - виртуальный конструктор:)

На основе этого паттерна делается Abstract Factory.

Далее я рассмотрю 2 варианта реализации паттерна

Обобщенный конструктор


В базовом классе определен статический метод создающий произвольные конкретные классы.
Изображение
#include <vector>
#include <iostream>

using namespace std;

enum Warrior_ID { Infantryman_ID=0, Archer_ID };

// Иерархия классов игровых персонажей
class Warrior
{
public:
  virtual void info() = 0;     
  virtual ~Warrior() {}
  // Параметризированный статический фабричный метод
  static Warrior* createWarrior( Warrior_ID id );
};

class Infantryman: public Warrior
{
public:
  void info() { 
    cout << "Infantryman" << endl; 
  }
};

class Archer: public Warrior
{
public:
  void info() { 
    cout << "Archer" << endl; 
  }
};

// Реализация параметризированного фабричного метода
Warrior* Warrior::createWarrior( Warrior_ID id )
{
  switch (id)
  {
  case Infantryman_ID:
    return new Infantryman();
  case Archer_ID:
    return new Archer(); 
  }
};


// Создание объектов при помощи параметризированного фабричного метода
int main()
{    
  vector<Warrior*> v;
  v.push_back( Warrior::createWarrior( Infantryman_ID));
  v.push_back( Warrior::createWarrior( Archer_ID));

  for(int i=0; i<v.size(); i++)
    v[i]->info();
}

Недостаток: класс Warrior должен знать обо всех своих наследниках, нехорошо.

Классический метод


Изображение
#include <vector>
#include <iostream>

using namespace std;

// Иерархия классов игровых персонажей
class Warrior
{
public:
  virtual void info() = 0;     
  virtual ~Warrior() {}
};

class Infantryman: public Warrior
{
public:
  void info() { 
    cout << "Infantryman" << endl; 
  };
};

class Archer: public Warrior
{
public:
  void info() { 
    cout << "Archer" << endl; 
  };     
};

// Фабрики объектов
class Factory
{
public:    
  virtual Warrior* createWarrior() = 0;
  virtual ~Factory() {}
};

class InfantryFactory: public Factory
{
public:    
  Warrior* createWarrior() { 
    return new Infantryman; 
  }
};

class ArchersFactory: public Factory
{
public:    
  Warrior* createWarrior() { 
    return new Archer; 
  }
};

// Создание объектов при помощи фабрик объектов
int main()
{    
  InfantryFactory* infantry_factory = new InfantryFactory;
  ArchersFactory*  archers_factory  = new ArchersFactory ;

  vector<Warrior*> v;
  v.push_back( infantry_factory->createWarrior());
  v.push_back( archers_factory->createWarrior());

  for(int i=0; i<v.size(); i++)
    v[i]->info();
  // ...
}

Недостаток: на каждый Warrior нужна своя конкретная фабрика

Вообщем мне этот паттерн в чистом виде не понравился (хотя Abstract Factory был хорош), поэтому я даже не стал придумывать свои примеры. Да и не сложен этот паттерн.

Ссылки


http://ru.wikipedia.org/wiki/Фабричный_метод_(шаблон_проектирования)
http://cpp-reference.ru/patterns/creational-patterns/factory-method/
http://alextretyakov.blogspot.ru/2012/04/shablon-factory-method.html

Ссылка

6 мая 2013

Продолжаю свое самообразование. Следующий паттерн:

Строитель (Builder)


Смысл этого паттерна - предоставить общий механизм конструирования объекта из более мелких объектов. Если еще проще, это типа как конструктор лего: есть детали определенной формы и цвета, комбинируя эти детали мы что-то собираем.

Реализация


Паттерн состоит из следующих участников:
- Builder - абстрактный интерфейс строителя
- ConcreteBuilder - конкретный строитель, наследуется от Builder
- Director - распорядитель. Конструирует объект используя конкретный строитель
- Product - продукт. Сложный продукт сконструированный конкретным строителем.

Вот как это выглядит:
Изображение

Код


Допустим делаю игру в жанре roguelike. Особенность данного жанра - случайная генерация подземелий. Надо сделать разные виды подземелья - лабиринт минотавра, египетские подземелья, муравейник, коллекторы, классическое rogue подземелье и т.д. Каждый из этих подземелий имеет свой алгоритм создания. Плюс подземелье состоит из частей - коридоры, комнаты, двери и украшения.

Конечно пример спорный, но другого я не смог быстро придумать:)

Теперь определим участников в виде классов.

Продуктом будет подземелье. Оно описывается классом Dungeon.

Мы "сделаем" два конструктора подземелья - лабиринт и улей. Для них нужен будет общий абстрактный класс. Итого:
- IDungeonBuilder - абстрактный строитель
- MazeBuilder - строитель лабиринта
- HiveBuilder - строитель улья

Плюс будет класс Director который и управляет всем процессом.

#include <stdio.h>

enum TypeDungeon
{
  MAZE = 0,
  HIVE,
};

// Продукт
class Dungeon
{
public:
  int room_tileset;
  int corridor_tileset;
  int door_tileset;
  int object_tileset;
};

// Абстрактный строитель
class IDungeonBuilder
{
public:
  virtual void BuildRoom() = 0;
  virtual void BuildCorridor() = 0;
  virtual void BuildDoor() = 0;
  virtual void BuildObject() = 0;
  
  void SetDungeon(Dungeon *dungeon) {m_dungeon = dungeon;}

  Dungeon* GetDungeon(){return m_dungeon;}

protected:
  Dungeon* m_dungeon;
};

// строитель лабиринта
class MazeBuilder : public IDungeonBuilder
{
public:
  void BuildRoom()
  {
    m_dungeon->room_tileset = MAZE;
  }
  void BuildCorridor()
  {
    m_dungeon->corridor_tileset = MAZE;
  }
  void BuildDoor()
  {
    m_dungeon->door_tileset = MAZE;
  }
  void BuildObject()
  {
    m_dungeon->object_tileset = MAZE;
  }
};

// строитель улья
class HiveBuilder : public IDungeonBuilder
{
public:
  void BuildRoom()
  {
    m_dungeon->room_tileset = HIVE;
  }
  void BuildCorridor()
  {
    m_dungeon->corridor_tileset = HIVE;
  }
  void BuildDoor()
  {
    m_dungeon->door_tileset = HIVE;
  }
  void BuildObject()
  {
    m_dungeon->object_tileset = HIVE;
  }
};

// распорядитель
class Director
{
public:
  void SetBuilder(IDungeonBuilder *builder) {m_builder = builder;}

  void SetDungeon(Dungeon *dungeon) {m_dungeon = dungeon;}
  Dungeon* GetDungeon(){return m_dungeon;}
    
  void Construct()
  {
    m_builder->SetDungeon(m_dungeon);
    m_builder->BuildRoom();
    m_builder->BuildCorridor();
    m_builder->BuildDoor();
    m_builder->BuildObject();

  }

private:
  Dungeon *m_dungeon;
  IDungeonBuilder *m_builder;
};

int main()
{
  Dungeon *dung = new Dungeon;
  Director director;

  director.SetDungeon(dung);

  IDungeonBuilder *builder = new MazeBuilder;
  
  director.SetBuilder(builder);
  director.Construct();

  printf("room num type - %d\n", dung->room_tileset);

  delete builder;

  builder = new HiveBuilder;

  director.SetBuilder(builder);
  director.Construct();

  printf("room num type - %d\n", dung->room_tileset);

  delete builder;

  return 0;
}

Ссылки


Как всегда в том порядке в котором я разбирал:
http://ru.wikipedia.org/wiki/Строитель_(шаблон_проектирования)
http://cpp-reference.ru/patterns/creational-patterns/builder/
http://www.firststeps.ru/theory/patt/r.php?3
http://trubetskoy1.narod.ru/patterns/builder.html
http://habrahabr.ru/post/86252/

Ссылка

4 мая 2013

Решил скрупулезно изучить все паттерны. Причина проста - открыл гоф... закрыл гоф, ибо не осилил беглым чтением. Но это не дело, поэтому поставил себе квест - изучить каждый паттерн досконально:)

Предупреждаю, все что я пишу, это мое личное мнение и мой личный опыт. Это может быть очень далеко или очень близко от истины, но не должно восприниматься как истина. Пишу здесь потому что это моя личная страница.

В качестве источников:

Э. Гамма Р. Хелм Р. Джонсон Дж. Влиссидес - Приемы объектно-ориентированного проектирования. Паттерны проектирования
Фримен Эр., Фримен Эл., Сьерра К., Бейтс Б. - Паттерны проектирования
Википедия
Гугл

Начну с первого патерна в GoF

Abstract Factory (абстрактная фабрика)


Это порождающий паттерн проектирования.
В GoF назначение данного патерна описано так:

Предоставляет интерфейс для создания семейств взаимосвязанных или взаимозависимых объектов, не специфицируя их конкретных классов.

Я лично первый раз прочитав такое описание нифига не понял:) И только разобрав код я наконец-то догадался о чем здесь написано.

Реализация


В реализации данного паттерна используются два вида классов - фабрика и продукт. Оба этих класса представлены из абстрактного и конкретного.
Вот как это выглядит:

AbstractFactory - абстрактная фабрика, содержит функции для создания абстрактного продукта.

ConcreteFactory - конкретная фабрика. наследуется от AbstractFactory. Реализует методы для создания конкретных продуктов

AbstractProduct - абстрактный продукт, является интерфейсом для конкретных продуктов (то есть конкретные продукты унаследованы от этого класса).

ConcreteProduct - конкретный продукт. наследуется от AbstractProduct.

Одна фабрика может создавать множество разных продуктов.

Вот хорошая диаграмма для понимания

+ Показать

Теперь хватит теории. Пора писать код.

Код

Вот допустим я создаю RTS (реалтаймовую стратегию), типа Warcraft 3. И в стратегии у нас есть две расы - гномы и эльфы. Юниты создаются в казарме. И вот тут-то и выявляется фишка данного паттерна. Но по порядку.

Давайте сначала определим что есть что. Начнем с фабрики.

В игре у нас есть казарма гномов и казарма эльфов. Обе они - казармы, так что пусть наследуются от базового класса. Получаем три класса:
IBarrackFactory - это абстрактная фабрика.
ElfBarrackFactory - это конкретная фабрика реализует казармы эльфов. наследуется от IBarrackFactory
GnomeBarrackFactory - это конкретная фабрика реализует казармы гномов. наследуется от IBarrackFactory

Также в игре есть два вида юнитов - эльфийский и гномий воины. Оба они наследуются от базового класса. Вот еще три класса:
IUnit - это абстрактный продукт.
ElfUnit - конкретный продукт, реализует эльфа
GnomeUnit - конкретный продукт, реализует гнома

Теперь код:

Юниты:

class IUnit
{
public:
  virtual std::string GetDesc() = 0;
};

class ElfUnit : public IUnit
{
public:
  std::string GetDesc()
  {
    return "Elf archer";
  }
};

class GnomeUnit : public IUnit
{
public:
  std::string GetDesc()
  {
    return "Gnome warrior";
  }
};

Казармы

class IBarrackFactory
{
public:
  virtual IUnit* Create() = 0;
};

class ElfBarrackFactory : public IBarrackFactory
{
public:
  IUnit* Create()
  {
    return new ElfUnit;
  }
};

class GnomeBarrackFactory : public IBarrackFactory
{
public:
  IUnit* Create()
  {
    return new GnomeUnit;
  }
};

Выполнение

void showUnit(IBarrackFactory *UnitFactory)
{
  IUnit *unit = UnitFactory->Create();
  printf("%s\n", unit->GetDesc().c_str());
  delete unit;
}

void main()
{
  IBarrackFactory *factory = new ElfBarrackFactory;
  printf("Elf army:\n");
  
  showUnit(factory);

  delete factory;

  factory = new GnomeBarrackFactory;
  printf("\nGnome army:\n");
  
  showUnit(factory);

  delete factory;
}

Как видите, мы используем showEnginery независимо от того, чьи сейчас казармы. То есть клиенту не нужно об этом знать

Ссылки


представлены в рекомендуемом мною порядке:
http://alextretyakov.blogspot.ru/2012/04/shablon-abstract-factory.html
http://cpp-reference.ru/patterns/creational-patterns/abstract-factory/
http://ru.wikipedia.org/wiki/Абстрактная_фабрика_(шаблон_проектирования)
http://www.gamedev.ru/community/oo_design/articles/?id=417
http://www.rsdn.ru/article/patterns/AbstractFactory.xml

Ссылка | Комментарии [16]

6 ноя 2012

Продолжаю велосипедить.  У меня система рендера (расположенная в библиотеке render.lib) имеет одну четкую задачу - вывод данных на экран... да, да, у всех так, да только не так:)

Я разбил систему рендера на 5 подсистем: device, resource, pipeline, frame, object. Точнее сейчас частично готовы только первые три, две остальные пока только в теории.

Device

Получилась самой простой. В данной системе пока что только один класс RenderDevice, но он является самым основным. Именно с ним работает движок. данный класс делает 4 задачи:
- инициализация DX и других систем рендера
- очищение памяти при выходе
- связывание других систем вместе (чтобы они могли взаимодействовать)
- обработка некоторых событий от окна (изменение окна к примеру). Точнее не так. все эти события уже обработаны в Window, здесь же вызываются методы по мере надобности:

if (m_wnd->IsResize())
      m_render->Resize();

То есть данный класс даже рисовать не умеет:) (нет методов Draw, BeginFrame и прочее)

class RenderDevice
  {
  public:
    bool CreateDevice(const DescRender &desc);
    void Close();

    void Resize();
        
    RenderResource* GetRenderRes() {return renderer_res;}
    RenderPipeline* GetRenderPipeline() {return render_pipeline;}

  private:
    void m_setsize();
    bool m_dxgi();
    bool m_initDX();
    
    DescRender m_desc;
    uint m_width;
    uint m_height;

    ID3D11Device *device;
    ID3D11DeviceContext *devicecontext;
    IDXGISwapChain *swapChain;
    RenderResource *renderer_res;
    RenderPipeline *render_pipeline;
  };

Здесь и далее весь код - сокращенный псевдокод (то есть убираются некоторые моменты)

Resource

Данная подсистема может ввести вас в заблуждение... она не имеет ничего общего с загрузкой и хранением файлов. Это полностью концепция Direct3D, то есть данные ресурсы, это куски памяти уже подготовленные для передачи на видеокарту (то есть кто-то из уже загрузил в память). Сейчас у меня ресурсы разделены на 4 группы - buffer (индексный, вершинный или константный), shader, state(blend, depth, vertexinput, rasterizer, sampler) и texture. Скорее всего будет добавлена еще одна группа - view (viewport, render target, depth stencil buffer)

Все эти ресурсы управляются и хранятся в классе RenderResource. Данный класс позволяет добавлять новые ресурсы возвращая их идентификатор. Но вообще возможно данный класс будет перенесен в ResourceManager

class RenderResource
{
public:
  TextureID AddRenderTarget();  
  TextureID AddRenderDepth();
  SamplerStateID AddSamplerState();
  BlendStateID AddBlendState();  
  DepthStateID AddDepthState();
  RasterizerStateID AddRasterizerState();

  VertexFormatID AddVertexFormat();
  VertexBufferID AddVertexBuffer();
  IndexBufferID AddIndexBuffer();
  
  TextureID AddTexture();  

  ShaderID AddShader();  
private:
  ID3D11Device *device;
  ID3D11DeviceContext *devicecontext;

  Array <Texture> textures;
  Array <Shader> shaders;
  Array <InputLayout> vertexFormats;
  Array <VertexBuffer> vertexBuffers;
  Array <IndexBuffer> indexBuffers;
  Array <SamplerState> samplerStates;
  Array <BlendState> blendStates;
  Array <DepthState> depthStates;
  Array <RasterizerState> rasterizerStates;
};

Pipeline
Данную систему я делал с упором на конвеер DX11. Получилось очень близко. Собственно в данной системе формулируется один DIP. То есть после того как мы добавили ресурсы в RenderResource, здесь мы их прикрепляем к конвееру и затем вызываем команду отрисовки.

class RenderPipeline
{
public:
  void ChangeRenderTarget();

  void Reset();

  void SetTexture();  
  void SetSamplerState();
  void SetShader();
  void SetVertexFormat();
  void SetVertexBuffer();
  void SetIndexBuffer();
  void SetBlendState()
  void SetDepthState();
  void SetRasterizerState();
  void SetShaderConstant();  
  void Apply();

private:
  InputAssemblerStage IA;
  OutputMergerStage OM;
  RasterizerStage RS;
  TempShaderStage TSS;
};

C помощью сетеров заполняем конвеер, и затем рисуем (DIP). Данный класс содержит члены стадий (так как с шейдерами у меня произошла архитектурная проблемка, за них пока только одна стадия TempShaderStage, потом для каждого вида также будет своя). Об стадиях говорить особо не буду, покажу только пример одной из них:

class InputAssemblerStage
{
public:
  void Reset();
  void ApplyState();

  void SetVertexFormat();
  void SetPrimitiveTopology();
  void SetVertexBuffer();
  void SetIndexBuffer();  
private:
  void ChangeVertexFormat();
  void ChangePrimitiveTopology();
  void ChangeVertexBuffer();
  void ChangeVertexBuffer();
  void ChangeIndexBuffer();

  ID3D11DeviceContext *context;
  RenderResource *res;
  
  VertexFormatID currentVertexFormat; 
  VertexFormatID selectedVertexFormat;
  VertexBufferID currentVertexBuffers[MAX_VERTEXSTREAM];
  VertexFormatID selectedVertexBuffers[MAX_VERTEXSTREAM];
  intptr selectedOffsets[MAX_VERTEXSTREAM];
  intptr currentOffsets[MAX_VERTEXSTREAM];
  IndexBufferID currentIndexBuffer;
  IndexBufferID selectedIndexBuffer;

  D3D11_PRIMITIVE_TOPOLOGY currTopology;
  D3D11_PRIMITIVE_TOPOLOGY selectTopology;
};

То есть RenderPipeline получив Set() зовет нужную команду из одного из своих членов.

Frame

Данная система пока не реализована и только в планах. Она будет отвечать за формирование одного кадра (напомню, pipeline формирует только один DIP). В системе будет один главный класс RenderFrame управляющий всеми другими классами Frame. Также будут следующие классы
RenderView - этот класс будет отвечать за конечное место вывода кадра (то есть именно этот класс рисует на экран). Будет содержать мировые матрицы и вьюпорт. Также здесь будет и Present().
RenderPass - по умолчанию будет один проход, рисующий сразу в RenderView. Но можно будет добавить несколько проходов рисования.

Кроме того данная система будет также собирать данные в пакеты (batch) и сортировать

Object

В данной системе будут более высокоуровневые классы - к примеру меши и материалы. Пока данная система даже не планировалась, так что сказать больше нечего. Возможно ее вообще не будет (меши будут в системе scene)


Кстати, недавно понял что две вещи по отношению к движку - мифы.
1 миф - современный движок нельзя написать одному. Unreal Engine 4 (до его анонса) писался одним человеком (его создателем... но слух непроверен). Unigine (кстати меня разочаровал, когда я его увидел, раньше я думал что он круче) был начат одним человеком. И всеми забытая (и кстати недавно ожившая) nebula 3 развивалась (и развивается)  одним человеком (судя по его блогу)
2 миф - движки без проектов нельзя писать... тут есть в лекциях гаймдева одна лекция где описано два подхода и "нет никакого движка, и пишется проект, а общий код переносится в следующие" только один из них. Кроме того оказывается CryEngine 1 является как раз примером такого вот движка в вакууме (анонс в 2000, выпуск в 2002, первая игра - 2004, ощутите разницу, для тех кто скажет что "ну вот они и делали far cry вместе с движком" скажу что движок был написан для презентация NVIDIA, затем на нем собирались делать две другие игры, и только потом far cry)

Нет я конечно даже близко не считаю что смогу до таких результатов дойти (даже не собираюсь - это огромные трудозатраты которые никак не окупятся), просто мотивацию подымаю:)

Выслушаю любые советы и предложения по этой схеме.

Ссылка | Комментарии [54]

26 окт 2012

Благодаря вики по DX11 (я же писал что вики нужна именно для меня) я смог превозмочь себя и довел таки движок до версии 0.1. При этом я сто-пятсотый раз переписал все с нуля, и на этот раз реально с нуля - больше нет никакого стороннего кода как раньше (вначале брался за основу ngene, потом Hieroglyph3, теперь только мои велосипеды - только хардкор). Это последнее переписывание движка с нуля, на ближайшие годы:) Архитектура идеальна и комфортна для меня, лучше уже не нужно, да и желания больше нет - зачем писать новый движок, если мне и этого будет хватать как когда-то Sapphire2D, так что карьеру движкописателя за ближайшие полгода собираюсь завершить:)

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

Напомню, движок затачивается под одно GAPI - DX11

Архитектурно модуль разделен на модули в виде статической библиотеке. Первоначально эту идею описывал здесь - http://www.gamedev.ru/pages/warzes/?id=6030

Новая архитектура
Идею модульности я долго мучал, столкнулся с кучей проблем (решение делать библиотеки динамическими не очень удачно оказалось), и все таки разбил

Вот так можно изобразить текущие системы:
Изображение

А вот так это выглядит в коде:
Изображение

Всего в коде 15445 строк чистого кода. С комментариями и прочим мусором почти 20000.

Каждая система четко выполняет свою работу и связана только с нижними системами- результат, добился однонаправленной зависимости в коде, то есть можно взять код common и спокойно юзать без остальных систем. Также и render можно спокойно юзать без Engine, но уже требуется math и common. Это очень удобно в поиске ошибок, а также визуально сокращает код так как каждая система имеет мало кода:
Common - 5927 строк кода
Math - 2311
Audio - 238
GUI - 999
Render - 4996
Engine - 974
Как видите - каждая система сама по себе очень маленькая (Common скорее всего будет разбит на новые системы)

Не зря мучался, даже теперь как-то тяжко смотреть на чужие движки где все смешано в одну кучу:)


Кроме того в этом движке я полностью добил идею разбиения игрового цикла на отдельные сцены (что позволяет легко делать главное меню, несколько игр и т.д. не извращаясь со всякими свитчами и процессами, кстати писал по этой вещи статью, но ее тут так и не опубликовали).

Минимальный код юзающий мой движок (не обращайте внимание на заголовки, еще нет главного заголовка для подключения):
Main.cpp

#include "../Engine/stdafx.h"
#include "../Engine/Application.h"
#include "TestModule.h"

int WINAPI WinMain(HINSTANCE hThisInst, HINSTANCE hLastInst, LPSTR lpszCmdLine, int nCmdShow)
{
  Application game;
  Config config;
  game.Create(config);
  game.AddModule(new TestModule(), "Test");
  game.SetActiveModule("Test");

  game.Go();

  game.Close();

  return 0;
}

TestModule.h

#pragma once

#include "../Engine/stdafx.h"
#include "../Engine/IModule.h"
#include "../Engine/InputCodes.h"
#include "../Engine/InputListener.h"
#include "../Engine/Engine.h"
#include "../Render/stdafx.h"
#include "../Render/Model.h"

using namespace Sapphire3D;

class TestModule : public IModule, public InputListener
{
public:
  void doInit();
  bool doRun();
  void doClose();

  virtual bool KeyPressed(const KeyEvent &arg);
  virtual bool KeyReleased(const KeyEvent &arg);

private:
  unsigned int m_idinput;
};

TestModule.cpp

#include "TestModule.h"

void TestModule::doInit()
{
  m_idinput = EngineInputMgr->AddListener(this);
}

bool TestModule::doRun()
{
  EngineRenderRes->clear(false, true, true);
  return true;
}

void TestModule::doClose()
{
  EngineInputMgr->RemoveListener(m_idinput);
}

bool TestModule::KeyPressed(const KeyEvent &arg)
{
  return false;
}

bool TestModule::KeyReleased(const KeyEvent &arg)
{
  return false;
}

Application - это самый главный класс. При этом обратите внимание - его не нужно наследовать как это сделано в 99% других движков (он для этого и не приспособлен). Этот класс полностью выполняет нужную работу движка и не требует вмешательства от пользователя. Но как вы понимаете, перед юзаньем мы должны настроить хотя бы размер экрана и прочее, для этого используется класс Config. После того как мы задали все стартовые настройки, мы должны инициализировать движок, и для этого используется метод Create(). Любые операции с движком до вызова этого метода запрещены (очень похоже на то что было в Sapphire2D). Этот метод создаст все системы движка.

А далее самое интересное - модули (бывшие сцены). В движке может быть бесконечное число модулей (в будущем реализую возможность создания подмодулей - пока не надо). Обычно я создаю модуль для главного меню и для самой игры. Такое архитектурное решение спасает меня от ужастных:

switch(CURRMODE)
{
case MAINMENU:
...
break;
case GAME:
...
break;
};

Знакомо, да? У меня такой проблемы нет вообще.

Или процессы:

Process *Menu;
Process *Game;

void Loop()
{
   procmgr.Add(Menu);
   procmgr.Add(Game);
}

Это уже ближе к моей реализации, но куча лишнего кода.

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

Реализация (кому вдруг интересно):

+ Показать