Эх, давно хотел попробовать написать какую-нибудь игру. К тому же уже был опыт работы с 3D графикой, как с DirectX, так и с OpenGL, но всё некогда, некогда. Тогда я решил воспользоваться одним из готовых 3D движков, и поковырять его. Что из этого вышло...
В общем, на хабре как то проскочила статья о отечественном движке NeoAxis. Давно его скачал, но вот только руки дошли поглядеть, что внутри.
Итак, движок, насколько я помню, основан на Ogre 3D, работа с графикой написана на неуправляемом C++, вокруг которого наворотили обёрток и дали нам. простым смертным, писать под него логику на любом .NET языке. Имеет несколько вариантов лицензий, в том числе и некоммерческую, которые отличаются ценой и возможностями. Также на сайте полно всяких руководств, документаций, туториалов, статей, как на английском, так и на русском языках. Скачать можно и хелпник, и SDK, и демки и даже их исходный код (сам я пробую некоммерческую лицензию). Вот я и начну с исходного кода демо проекта (где уже включена RTS), который буду чуть чуть модифицировать, затачивая под свои нужды. Оговорюсь сразу - я не делаю полноценную стратегию, я просто хочу оценить насколько это затратно и реально ли вообще.
Как известно, RTS состоит из нескольких компонентов:
1. Карта. Для этого разработчики движка заботливо приложили к SDK специальное приложение - редактор карт. Он позволяет работать с объектами карты, с персонажами и даже запускать симуляцию карты (то есть как она будет работать в игре). Также в редактор карт встроен редактор логики - то есть можно скриптовать различные сценарии не отходя от станка, так сказать.
2. Ресурсы.
К ресурсам относятся следующие типы игровых объектов:
3. Конфиг. Тут можно указывать настройки движка - рендера, звука и тд. Я вынес это как отдельный пункт, так как в поставке есть программа-конфигуратор.
4. Дополнительные инструменты
5. Ну, и, собственно, код
Для ясности, приведу весь список инструментов, что идут с SDK
Теперь к коду.
Сразу в солюшен включены несколько проектов:
Отлично. Теперь при запуске игры, она покажет отредактированное окно и кнопка "Играть" запустит нужную мне карту.
Итак, на данный момент у нас есть карта с юнитами, есть главное меню, не хватает только AI. Не, ну, конечно, сейчас враги будут в нас стрелять и наши войска будут стрелять в них и кто то, очевидно, погибнет, но хочется как то этим процессом управлять. Для этого заглянем в проект GameEntities, а именно в класс RTSUnitAI.
Насколько я понял, первый класс используется в редакторе ресурсов. А вот во втором творится самое чудесное - логика AI. Там довольно много кода, потому я решил написать класс логики персонажей с нуля, конечно, подглядывая в этот класс. Итак, начнем!
Сперва сделаем пустое AI
После этого, в папке \Data\Types\RTSSpecific\AIs найдем файл DefaultRTSUnitAI.type, вот его содержимое
Заменим на 
Запустив игру, можно заметить, что юниты теперь вообще ни на что не реагируют. Это понятно, ведь AI у них сейчас пустой! А давайте мотивируем их на что нибудь! :)
Самое главное, это перегрузить метод
тут варится вся логика нашего персонажа. Но перед этим нужно провести подготовительные работы. Так как я впервые работаю с этим движком, я буду потихоньку копировать логику из AI по умолчанию и, где надо, её затачивать для себя. 
Итак, самое первое - это инициализация вооружения персонажа. Оружие используется как минимум для того, чтобы определить, может ли персонаж стрелять, и если может, то из какой точки выпускать анимированную пулю. Далее. нужно определиться с задачей, которую он выполняет - если текущая задача есть, то пусть действует по ней. Если задачи закончились - пусть ищет себе занятие сам.
Весь остальной код, в основном, обслуживает этот метод. Думаю, ещё стоит привести код метода, который выполняет текущую задачу
Что касается структуры Task - её мы тоже определили в коде
Теперь можно поработать и с AI. Например, у нас есть функция, которая определяет приоритет для атаки. Немного её модифицировав, можно повысить приоритет атаки робота-медика. То есть юниты будут атаковать сначала его, а потом военного робота.
Помимо всего этого, в игру можно добавить другие юниты и прописать им интеллект, изменить баланс игры, да вообще много чего можно сделать. Вот что у меня получилось в результате.
 
Итак, имея готовый движок и демки, можно легко дорабатывать игру под себя. Ценители графики могут добавлять различные эффекты, реалистичные модели, анимации и много всего; искусственный интеллект тоже поддаётся изменению, так и вообще полной замене, а использование технологии .NET сильно упрощает работу. В заключение также хочется отметить, что трудозатраты на создание игры в данном случае были практически нулевые - 2 часа на изучение и кодинг и час на написание статьи. Это говорит о низком пороге вхождения в технологию. На этом всё. Всем спасибо.
В общем, на хабре как то проскочила статья о отечественном движке NeoAxis. Давно его скачал, но вот только руки дошли поглядеть, что внутри.
Итак, движок, насколько я помню, основан на Ogre 3D, работа с графикой написана на неуправляемом C++, вокруг которого наворотили обёрток и дали нам. простым смертным, писать под него логику на любом .NET языке. Имеет несколько вариантов лицензий, в том числе и некоммерческую, которые отличаются ценой и возможностями. Также на сайте полно всяких руководств, документаций, туториалов, статей, как на английском, так и на русском языках. Скачать можно и хелпник, и SDK, и демки и даже их исходный код (сам я пробую некоммерческую лицензию). Вот я и начну с исходного кода демо проекта (где уже включена RTS), который буду чуть чуть модифицировать, затачивая под свои нужды. Оговорюсь сразу - я не делаю полноценную стратегию, я просто хочу оценить насколько это затратно и реально ли вообще.
Как известно, RTS состоит из нескольких компонентов:
1. Карта. Для этого разработчики движка заботливо приложили к SDK специальное приложение - редактор карт. Он позволяет работать с объектами карты, с персонажами и даже запускать симуляцию карты (то есть как она будет работать в игре). Также в редактор карт встроен редактор логики - то есть можно скриптовать различные сценарии не отходя от станка, так сказать.
2. Ресурсы.
К ресурсам относятся следующие типы игровых объектов:
- трехмерные модели,
 - физические модели,
 - материалы,
 - текстуры,
 - интерфейсы,
 - системы частиц,
 - описания шрифтов,
 - звуки,
 - видео
 
3. Конфиг. Тут можно указывать настройки движка - рендера, звука и тд. Я вынес это как отдельный пункт, так как в поставке есть программа-конфигуратор.
4. Дополнительные инструменты
5. Ну, и, собственно, код
Для ясности, приведу весь список инструментов, что идут с SDK
-  
Редактор ресурсов - Редактор ресурсов предназначен для редактирования ресурсов проекта. Сюда входят, главным образом, трехмерные модели, карты, материалы, текстуры, звуки и прочее.
 -  
Редактор карт - Редактор карт предназначен для создания и редактирования игровых карт.
 -  
Конфигуратор - Утилита для настройки параметров движка.
 -  
Deployment Tool - Инструмент для подготовки конечного продукта.
 -  
Компилятор шейдеров - Компилятор кэша шейдеров.
 
Теперь к коду.
Сразу в солюшен включены несколько проектов:
- GameCommon - Различные классы проекта, такие как, описание типов материалов, сетевые сервисы проекта, класс консоли движка, пользовательские гуи классы.
 - GameEntities - Описание игровых классов и всей логики игры.
 - Game - Точка входа приложения. Инициализация движка, классы для реализации структуры проекта, навигации игровых экранов и взаимодействия с пользователем.
 - ChatExample - Пример реализации сетевого чата на базе Windows Forms.
 - DedicatedServer - Приложение для создания выделенного сервера.
 - WinFormsAppExample - Пример интеграции движка в Windows Forms приложение.
 - WPFAppExample - Пример интеграции движка в WPF приложение.
 
- //button handlers - ненужное просто закаментил
 - //( (Button)window.Controls[ "Run" ] ).Click += Run_Click;
 - //( (Button)window.Controls[ "RunVillageDemo" ] ).Click += RunVillageDemo_Click;
 - //( (Button)window.Controls[ "Multiplayer" ] ).Click += Multiplayer_Click;
 - //( (Button)window.Controls[ "Maps" ] ).Click += Maps_Click;
 - //( (Button)window.Controls[ "LoadSave" ] ).Click += LoadSave_Click;
 - ( (Button)window.Controls[ "Options" ] ).Click += Options_Click;
 - //( (Button)window.Controls[ "Profiler" ] ).Click += Profiler_Click;
 - //( (Button)window.Controls[ "GuiTest" ] ).Click += GuiTest_Click;
 - //( (Button)window.Controls[ "About" ] ).Click += About_Click;
 - ( (Button)window.Controls[ "Exit" ] ).Click += Exit_Click;
 - // Добавляю обработчик на свою кнопку, которая грузит нужную мне карту
 - ((Button) window.Controls["Play"]).Click += delegate(Button sender)
 - {
 - var file = VirtualDirectory.GetFiles(@"Maps\RTSDemo\", "Map.map", SearchOption.AllDirectories)[0];
 - GameEngineApp.Instance.SetNeedMapLoad(file);
 - };
 
Итак, на данный момент у нас есть карта с юнитами, есть главное меню, не хватает только AI. Не, ну, конечно, сейчас враги будут в нас стрелять и наши войска будут стрелять в них и кто то, очевидно, погибнет, но хочется как то этим процессом управлять. Для этого заглянем в проект GameEntities, а именно в класс RTSUnitAI.
- /// <summary>
 - /// Defines the <see cref="RTSUnitAI"/> entity type.
 - /// </summary>
 - public class RTSUnitAIType : AIType
 - {
 - }
 - public class RTSUnitAI : AI
 - {
 - ................
 - }
 
Сперва сделаем пустое AI
- namespace GameEntities
 - {
 - public class RTSUnitMegaAIType : AIType
 - {
 - }
 - public class RTSUnitMegaAI: AI
 - {
 - RTSUnitMegaAIType _type = null; public new RTSUnitMegaAIType Type { get { return _type; } }
 - }
 - }
 
После этого, в папке \Data\Types\RTSSpecific\AIs найдем файл DefaultRTSUnitAI.type, вот его содержимое
- type DefaultRTSUnitAI
 - {
 - class = RTSUnitAI
 - }
 
- type DefaultRTSUnitAI
 - {
 - class = RTSUnitMegaAI
 - }
 
Запустив игру, можно заметить, что юниты теперь вообще ни на что не реагируют. Это понятно, ведь AI у них сейчас пустой! А давайте мотивируем их на что нибудь! :)
Самое главное, это перегрузить метод
- protected override void OnTick()
 
Итак, самое первое - это инициализация вооружения персонажа. Оружие используется как минимум для того, чтобы определить, может ли персонаж стрелять, и если может, то из какой точки выпускать анимированную пулю. Далее. нужно определиться с задачей, которую он выполняет - если текущая задача есть, то пусть действует по ней. Если задачи закончились - пусть ищет себе занятие сам.
- /// <summary>
 - /// Самый мозг юнита
 - /// </summary>
 - protected override void OnTick()
 - {
 - base.OnTick();
 - // обновляем вооружение
 - if (_initialWeapons == null)
 - UpdateInitialWeapons();
 - // Выполняем шаг для текущей заачи
 - TickTasks();
 - // Если задачи заканчиваются, ищем новую задачу
 - if ((_currentTask.Type == Task.Types.Stop ||
 - _currentTask.Type == Task.Types.BreakableMove ||
 - _currentTask.Type == Task.Types.BreakableAttack ||
 - _currentTask.Type == Task.Types.BreakableRepair
 - ) && _tasks.Count == 0)
 - {
 - _inactiveFindTaskTimer -= TickDelta;
 - if (_inactiveFindTaskTimer <= 0)
 - {
 - _inactiveFindTaskTimer += 1.0f;
 - if (!InactiveFindTask())
 - _inactiveFindTaskTimer += .5f;
 - }
 - }
 - }
 
Весь остальной код, в основном, обслуживает этот метод. Думаю, ещё стоит привести код метода, который выполняет текущую задачу
- /// <summary>
 - /// Выполнение задачи
 - /// </summary>
 - protected virtual void TickTasks()
 - {
 - // Наш юнит
 - var controlledObj = ControlledObject;
 - if (controlledObj == null)
 - return;
 - switch (_currentTask.Type)
 - {
 - // остановить юнит
 - case Task.Types.Stop:
 - controlledObj.Stop();
 - break;
 - // движение
 - case Task.Types.Move:
 - case Task.Types.BreakableMove:
 - if (_currentTask.Entity != null)
 - {
 - controlledObj.Move(_currentTask.Entity.Position);
 - }
 - else
 - {
 - var pos = _currentTask.Position;
 - if ((controlledObj.Position.ToVec2() - pos.ToVec2()).LengthFast() < 1.5f &&
 - Math.Abs(controlledObj.Position.Z - pos.Z) < 3.0f)
 - {
 - DoNextTask();
 - }
 - else
 - controlledObj.Move(pos);
 - }
 - break;
 - // Атакуем или чиним
 - case Task.Types.Attack:
 - case Task.Types.BreakableAttack:
 - case Task.Types.Repair:
 - case Task.Types.BreakableRepair:
 - {
 - // ремонт
 - if ((_currentTask.Type == Task.Types.Repair ||
 - _currentTask.Type == Task.Types.BreakableRepair)
 - && _currentTask.Entity != null)
 - {
 - // проверяем, если жизней у робота достаточно, то прекращаем его чинить
 - if (Math.Abs(_currentTask.Entity.Life - _currentTask.Entity.Type.LifeMax) < 0.000001)
 - {
 - DoNextTask();
 - break;
 - }
 - }
 - // Оптимальное расстояние для возможности ремонта
 - var needDistance = controlledObj.Type.OptimalAttackDistanceRange.Maximum;
 - // позиция юнита, которого чиним
 - Vec3 targetPos;
 - if (_currentTask.Entity != null)
 - targetPos = _currentTask.Entity.Position;
 - else
 - targetPos = _currentTask.Position;
 - // расстояние между нашим юнитом и целью
 - var distance = (controlledObj.Position - targetPos).LengthFast();
 - // если это расстояние не равно 0
 - if (Math.Abs(distance - 0) > 0.000001)
 - {
 - var lineVisibility = false;
 - // если луч для лечения может достать до цели
 - if (distance < needDistance)
 - {
 - lineVisibility = true;
 - var start = _initialWeapons[0].Position;
 - var ray = new Ray(start, targetPos - start);
 - var piercingResult = PhysicsWorld.Instance.RayCastPiercing(
 - ray, (int)ContactGroup.CastOnlyContact);
 - foreach (var result in piercingResult)
 - {
 - var obj = MapSystemWorld.GetMapObjectByBody(result.Shape.Body);
 - if (obj != null && obj == _currentTask.Entity)
 - break;
 - if (obj != controlledObj)
 - {
 - lineVisibility = false;
 - break;
 - }
 - }
 - }
 - // если цель не в прямой видимости, то нужно повернуться к ней, или двигаться в её сторону
 - if (lineVisibility)
 - {
 - controlledObj.Stop();
 - var character = controlledObj as RTSCharacter;
 - if (character != null)
 - character.SetLookDirection(targetPos);
 - }
 - else
 - {
 - controlledObj.Move(targetPos);
 - }
 - // тут проверяем, можем ли задействовать оружие. Если можем - то стреляем
 - if (lineVisibility)
 - {
 - foreach (Weapon weapon in _initialWeapons)
 - {
 - var pos = targetPos;
 - var gun = weapon as Gun;
 - if (gun != null && _currentTask.Entity != null)
 - gun.GetAdvanceAttackTargetPosition(false, _currentTask.Entity, false, out pos);
 - weapon.SetForceFireRotationLookTo(pos);
 - if (weapon.Ready)
 - {
 - var range = weapon.Type.WeaponNormalMode.UseDistanceRange;
 - if (distance >= range.Minimum && distance <= range.Maximum)
 - weapon.TryFire(false);
 - range = weapon.Type.WeaponAlternativeMode.UseDistanceRange;
 - if (distance >= range.Minimum && distance <= range.Maximum)
 - weapon.TryFire(true);
 - }
 - }
 - }
 - }
 - }
 - break;
 - }
 - }
 
- public struct Task
 - {
 - [Entity.FieldSerializeAttribute]
 - [DefaultValue(RTSUnitAI.Task.Types.None)]
 - Types type;
 - [Entity.FieldSerializeAttribute]
 - [DefaultValue(typeof(Vec3), "0 0 0")]
 - Vec3 position;
 - [Entity.FieldSerializeAttribute]
 - DynamicType entityType;
 - [Entity.FieldSerializeAttribute]
 - Dynamic entity;
 - public enum Types
 - {
 - None,
 - Stop,
 - BreakableAttack,//for automatic attacks
 - Hold,
 - Move,
 - BreakableMove,//for automatic attacks
 - Attack,
 - Repair,
 - BreakableRepair,//for automatic repair
 - BuildBuilding,
 - ProductUnit,
 - SelfDestroy,//for cancel build building
 - }
 - public Task(Types type)
 - {
 - this.type = type;
 - this.position = new Vec3(float.NaN, float.NaN, float.NaN);
 - this.entityType = null;
 - this.entity = null;
 - }
 - public Task(Types type, Vec3 position)
 - {
 - this.type = type;
 - this.position = position;
 - this.entityType = null;
 - this.entity = null;
 - }
 - public Task(Types type, DynamicType entityType)
 - {
 - this.type = type;
 - this.position = new Vec3(float.NaN, float.NaN, float.NaN);
 - this.entityType = entityType;
 - this.entity = null;
 - }
 - public Task(Types type, Vec3 position, DynamicType entityType)
 - {
 - this.type = type;
 - this.position = position;
 - this.entityType = entityType;
 - this.entity = null;
 - }
 - public Task(Types type, Dynamic entity)
 - {
 - this.type = type;
 - this.position = new Vec3(float.NaN, float.NaN, float.NaN);
 - this.entityType = null;
 - this.entity = entity;
 - }
 - public Types Type
 - {
 - get { return type; }
 - }
 - public Vec3 Position
 - {
 - get { return position; }
 - }
 - public DynamicType EntityType
 - {
 - get { return entityType; }
 - }
 - public Dynamic Entity
 - {
 - get { return entity; }
 - }
 - public override string ToString()
 - {
 - string s = type.ToString();
 - if (!float.IsNaN(position.X))
 - s += ", Position: " + position.ToString();
 - if (entityType != null)
 - s += ", EntityType: " + entityType.Name;
 - if (entity != null)
 - s += ", Entity: " + entity.ToString();
 - return s;
 - }
 - }
 
Теперь можно поработать и с AI. Например, у нас есть функция, которая определяет приоритет для атаки. Немного её модифицировав, можно повысить приоритет атаки робота-медика. То есть юниты будут атаковать сначала его, а потом военного робота.
- /// <summary>
 - /// Устанавливает приоритет для атаки
 - /// </summary>
 - /// <param name="obj"></param>
 - /// <returns></returns>
 - protected float GetAttackObjectPriority(Unit obj)
 - {
 - if (ControlledObject == obj)
 - return 0;
 - if (obj.Intellect == null)
 - return 0;
 - //RTSConstructor specific
 - if (ControlledObject.Type.Name == "RTSConstructor")
 - {
 - if (Faction == obj.Intellect.Faction)
 - {
 - if (obj.Life < obj.Type.LifeMax)
 - {
 - Vec3 distance = obj.Position - ControlledObject.Position;
 - float len = distance.LengthFast();
 - return 1.0f / len + 1.0f;
 - }
 - }
 - }
 - else
 - {
 - if (Faction != null && obj.Intellect.Faction != null && Faction != obj.Intellect.Faction)
 - {
 - var distance = obj.Position - ControlledObject.Position;
 - var len = distance.LengthFast();
 - var result = 1.0f / len + 1.0f;
 - // повышаем приоритет атаки на робота-медика
 - if (obj.Type.Name == "RTSConstructor") result = 1.1f/ len + 1.0f;
 - return result;
 - }
 - }
 - return 0;
 - }
 
Помимо всего этого, в игру можно добавить другие юниты и прописать им интеллект, изменить баланс игры, да вообще много чего можно сделать. Вот что у меня получилось в результате.
Итак, имея готовый движок и демки, можно легко дорабатывать игру под себя. Ценители графики могут добавлять различные эффекты, реалистичные модели, анимации и много всего; искусственный интеллект тоже поддаётся изменению, так и вообще полной замене, а использование технологии .NET сильно упрощает работу. В заключение также хочется отметить, что трудозатраты на создание игры в данном случае были практически нулевые - 2 часа на изучение и кодинг и час на написание статьи. Это говорит о низком пороге вхождения в технологию. На этом всё. Всем спасибо.
Статья понравилась! Я тоже всегда хотел стратегию написать))) Все время слежу за проектом springrts.com - опенсорс стратегия.
ОтветитьУдалитьИ эта статья мне понравилась тем, что показывает, какие примерно есть тулзы (редакторы карт, материалов...) и из каких компонентов состоит стратегическая игра. Всегда полезно иметь взгляд на всё это свысока.
Выложите, пожалуйста, ссылку на исходник.
ОтветитьУдалитьВсе исходники, что я писал, тут, в статье :) Чтобы хоть что то скомпилировать, Вам нужно скачать движок.
Удалить* купить =)
УдалитьПочему, есть же некоммерческая версия SDK
Удалить