Эх, давно хотел попробовать написать какую-нибудь игру. К тому же уже был опыт работы с 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
Удалить