Посадил дед репку… а из нее поползли тентакли.
Введение
Статья господина FrozmatGames побудила меня поторопиться с данной статьей, в которой пойдет речь о реализации прототипа к игре на тему генетики. Заранее прошу прощения за возможную сыроватость текста. Времени маловато для «причесывания». Шлите хабрапочтой гневности про очепятки и прочие текстовые огрехи — постараюсь все поправить.
В статье я постараюсь кратко описать созданный демо-концепт с симуляцией роста органики. В начале опишу саму идею, по которой создавалось демо. Потом опишу какие инструменты начинал использовать и на чем остановился в итоге. Ну и закончу описанием некоторых моментов в реализации, демонстрацией, а также списком вещей, которые хотелось бы реализовать далее.
Как всегда, код выложен на Github.
Идея игры и Прототип #1
Главным источников вдохновления для меня являются миры братьев Стругацких (А., Б. и Дж.;), а также несколько статей на хабре (см. список ниже).
- «Живые графы» — выращивание графов на клеточных автоматах с примерами на Silverlight
- Обзор методов эволюции нейронных сетей
- Neuro Evolution of Augmenting Topologies
- Еще об эволюции гоночных автомобилей
Базовая идея — игровой мир, в котором все или почти все наполнено жизнью, не строится, не собирается, не изготавливается, а рождается, развивается и растет. В общем, смесь Terraria со Spore. Это если вкратце об идее игры. Подробнее же пока рассказывать не буду — перед этим надо свои мысли в порядок привести.
Начать решил с реализации прототипа, в котором можно будет понаблюдать за рождением и развитием некоего абстрактного растения (рабочее название в заголовке статьи). На рисунках ниже представлены зарисовки задуманного прототипа.
На этой зарисовке показана начальная стадия развития объекта. На этой стадии из ядра «вырастают» косточки и струны их соединяющие. Косточки и струны могут делиться или добавляться новые из ядра. По мере роста части конструкции могут изменять свою геометрию, физические характеристики.
Деление и вставка — это две операции, которые хорошо описаны в "Neuro Evolution of Augmenting Topologies". Их можно будет шифровать генами, что позволит мне в будущем использовать генетические алгоритмы для эволюционирования мира.
Physics2D.Net + WPF Render vs Unity3D 4.3.0
Сначала реализовывать прототип было решено с использованием Physics2D.Net. С ним работать вроде бы несложно (можете почитать простенький туториал на вики-старничке):
PhysicsEngine engine = new PhysicsEngine();
engine.BroadPhase = new Physics2DDotNet.Detectors.SelectiveSweepDetector();
engine.Solver = new Physics2DDotNet.Solvers.SequentialImpulsesSolver();
Запуск и останов симуляции производится через объект таймер (PhysicsTimer):
PhysicsTimer timer = new PhysicsTimer(engine.Update, .01f);
timer.IsRunning = true;
Но возможностей, конечно маловато. К тому же проблемой стала отрисовка сцены. В качестве простейшего решения был использован рендеринг на WriteableBitmap (см. код в репозитории). В последствии я от этого подхода отказался, т.к. подозревал, что будут сложности при замене рендеринга чем-то более производительным. Лучше сразу изучать что-то более функциональное, решил я. Если же кому-то будет интересно почитать про Physics2D.Net, то отпишитесь в комментариях — попробую найти время на него.
После статьи о релизе Unity3D 4.3.0, в котором реализован нативный режим 2D, было решено попробовать реализовать демо на нем. Проекты на Unity3D отличаются от привычной для меня архитектуры ООП своим массовым использованием компонентного подхода организации кода. Потому код из ветки с Physics2D решил не использовать, а начать реализацию с нуля.
В качестве основы своего концепта я взял 2D демо-проект (вот этот вот). Создал свои спрайты для основных частей игрового объекта и сценарии поведения. Также в проект были добавлены реализации различных хэлперов.
Подозревал, что с традиционным ООП подходом в Unity3D будет сложновато. Полиморфизм в сценариях поведения не работает. Но вообще, проблем с традиционным ООП оказалось не так уж и много… может пока. При работе с солюшеном я пользуюсь не только Mono Develop, но и Visual Studio 2012, т.к. редактор C# в ней качественнее и стабильнее. Столкнулся с проблемами Mono Develop с автоотступами, перетаскиванием кусков кода, автозакрытием скобок и прочими мелочами, которых достаточно много и они напрягают. Но дебажить через VS я пока не научился. К стати говоря, с дебагом и у Mono Develop проблемы. Почему-то в дебаге выгружаются пространства имен с enum, из-за чего типы игровых объектов непоказываются в дебаге, их приходится дублировать в строковых переменных, что мне очень не нравится. Возможно я просто не знаю какой-то хитрости или зря выносил enum'ки в отдельное пространство имен.
Реализация
Что же реализовано на данный момент:
- Добавлены три вида префабов: ядро (Core), узел (Node) и кость (Bone)
- Узлов и косточек несколько видов, ядро пока одного вида
- Реализована загрузка, расшифровка и репликация ДНК, а также генов в ее составе. См. скрипты Seed, DnaProcessor, GeneProcessor в репозитории
- Расшифрованные гены применяются к текущей части игрового объекта при удовлетворении условий применимости
- Реализованы следующие условия применимости гена: текущая позиция в дереве элементов объекта (± случайный допуск); подтип родительского элемента; время активации гена.
- Для генов костей и узлов реализовано добавление дочерних элементов
- Скрипты, реализующие различные стадии роста, общаются друг с другом посредством отправки широковещательных сообщений (GameObject.SendMessage) для компонентов текущего GameObject.
- Реализована простая анимация при добавлении узлов.
- Реализованы два синглтон-хэлпера для работы с генами (GenesManager) и префабами в ресурсах (PrefabsManager).
Теперь чуть подробнее жизненный цикл в демо.
Корнем выращиваемого объекта является ядро (префаб Core). У этого объекта имеется скрип Seed который выполняет загрузку из ресурсов «dna.txt».
using UnityEngine;
using System.Collections;
using Fukami.Helpers;
public class Seed : MonoBehaviour
{
public string Dna;
void Start(){
var dnaAsset = Resources.Load<TextAsset>("dna");
Dna = dnaAsset.text.Replace("rn","*");
}
void OnSeedDnaStringRequested (Wrap<string> dna)
{
dna.Value = Dna;
dna.ValueSource = "Seed";
}
}
В этом файле содержится полная ДНК «репы».
// node,[Node type],[Bone Type],[Base depth],[Depth tolerance],[Grow Time] node,1,0,0,0,07A node,1,0,0,0,080 node,2,0,0,0,100 bone,1,0,4,2,190 bone,1,0,4,2,15E node,3,0,3,2,96 bone,1,0,0,5,C8 bone,1,0,2,1,FA bone,1,0,3,1,12C node,2,0,3,2,Fa node,1,0,7,3,80 bone,1,0,8,2,128
Также к ядру присоединен скрип DnaProcessor, который на старте проверяет, есть ли родитель у текущего элемента.
void Start()
{
_age = 0.0f;
var dna = new Wrap<string>();
if (gameObject.transform.parent != null) {
var gen = new Wrap<int>();
gameObject.transform.parent.SendMessage("OnDnaGenRequested", gen);
_generation = gen.IsSet ? gen.Value + 1 : 0;
gameObject.transform.parent.SendMessage("OnDnaStringRequested", dna);
}
else {
SendMessage("OnSeedDnaStringRequested", dna);
}
if (dna.IsSet)
{
DnaString = dna.Value;
}
}
Если родителя нет, то DnaProcessor отправляет сообщение «OnSeedDnaStringRequested» с запросом ДНК от скрипта Seed. В противном случае будет отправлено сообщение «OnDnaStringRequested» для получения ДНК от такого же DnaProcessor в родительском элементе. Также DnaProcessor запрашивает у родителя текущее положение в дереве (глубину, поколение), чтобы передать это значение генам.
Жизненный цикл гена контролируется скриптом «GeneProcessor». Этот сценарий очень прост, он отсчитывает время активации и потом посылает сообщение «OnApplyGene».
void Update () {
if (Gene != null)
{
_age += Time.deltaTime;
}
if (_age >= Gene.GrowTime)
{
enabled = false;
SendMessage("OnApplyGene", Gene);
return;
}
}
Сообщение это будет принято либо «BonesApplicant» (если текущий элемент является узлом), либо «NodesApplicant» в случае косточки. Оба эти скрипта в обработчике сообщения «OnApplyGene» выполняют проверку условий и правил применимости гена.
bool GetIsApplicable(GeneData gene)
{
// Part Type check
if (gene.GeneType != "node" && gene.GeneType != "stats")
{
return false;
}
// Subtype check
if (gene.ApplicantSubtype != 0 && gene.ApplicantSubtype != Subtype) {
return false;
}
// Generation check
var gen = Generation;
if (gen != gene.BaseDepth)
{
var actDistance = Mathf.Abs(gen - gene.BaseDepth);
if (UnityEngine.Random.Range(0f, 1f) < actDistance / (gene.DepthTolerance + 1)) {
return false;
}
}
return true;
}
void OnApplyGene(GeneData gene)
{
if (!GetIsApplicable(gene)) return;
switch (gene.GeneType.ToLower())
{
case "bone":
AddBone(gene);
break;
case "joint":
AddJoint(gene);
break;
case "stats":
AddStats(gene);
break;
default:
break;
}
}
Если все в порядке и есть свободные слоты (список ChildSlot), то для гена создается дочерний элемент.
private void AddBone(GeneData gene)
{
var slot = Slots.FirstOrDefault(s => !s.IsOccupied);
if (slot == null)
{
return;
}
slot.IsOccupied = true;
var bonePrefab = PrefabsManager.Instance.LoadPrefab(string.Format("{0}{1}", gene.GeneType, gene.Subtype));
var newBody = (GameObject)Instantiate(bonePrefab,
gameObject.transform.position,
gameObject.transform.rotation *
Quaternion.AngleAxis(slot.Angle, Vector3.forward));
newBody.transform.parent = gameObject.transform;
newBody.SetActive(true);
//var slide = newBody.AddComponent<SliderJoint2D>();
var slide = newBody.AddComponent<HingeJoint2D>();
slide.connectedBody = gameObject.GetComponent<Rigidbody2D>();
slide.connectedAnchor = new Vector2(slot.X, slot.Y);
//slide.limits = new JointTranslationLimits2D { min = -0.1f, max = 0.1f };
slide.limits = new JointAngleLimits2D { min = -1.0f, max = 1.0f };
slide.useLimits = true;
gameObject.SendMessage("OnChildAdded", newBody, SendMessageOptions.DontRequireReceiver);
}
bool GetIsApplicable(GeneData gene)
{
// Part Type check
if (gene.GeneType != "bone" && gene.GeneType != "stats" && gene.GeneType != "joint")
{
return false;
}
// Subtype check
if (gene.ApplicantSubtype != 0 && gene.ApplicantSubtype != Subtype) {
return false;
}
// Generation check
var gen = Generation;
if (gen != gene.BaseDepth)
{
var actDistance = Mathf.Abs(gen - gene.BaseDepth);
if (UnityEngine.Random.Range(0f, 1f) < actDistance / (gene.DepthTolerance + 1)) {
return false;
}
}
return true;
}
private void AddNode(GeneData gene)
{
var slot = Slots.FirstOrDefault(s => !s.IsOccupied);
if (slot == null) return;
slot.IsOccupied = true;
var nodePrefab = PrefabsManager.Instance.LoadPrefab(string.Format("{0}{1}", gene.GeneType, gene.Subtype));
var newBody = (GameObject)Instantiate(nodePrefab,
gameObject.transform.position,
Quaternion.FromToRotation(Vector3.right, Vector3.up) *
//Quaternion.AngleAxis(Random.Range(-slot.Angle, slot.Angle), Vector3.forward));
Quaternion.AngleAxis(slot.Angle, Vector3.forward));
newBody.transform.parent = gameObject.transform;
newBody.SetActive(true);
var hinge = newBody.AddComponent<HingeJoint2D>();
hinge.connectedBody = gameObject.GetComponent<Rigidbody2D>();
hinge.connectedAnchor = new Vector2(slot.X, slot.Y);
hinge.limits = new JointAngleLimits2D { min = -1.0f, max = 1.0f };
hinge.useLimits = true;
newBody.AddComponent<HingeSmoothPos>();
gameObject.SendMessage("OnChildAdded", newBody, SendMessageOptions.DontRequireReceiver);
}
Дочерний элемент инстанциируется через PrefabsManager, который выбирает префаб по строковому типу (bone или node) и постфиксу подтипа префаба. Получается, например, «node1», «node2» и т.п. К узлам дополнительно прикреплен скрипт DieIfNoChildren, который просто уничтожает узел, если к нему через определенное время не будет присоединен дочерний элемент.
Про многие моменты есть что рассказать отдельно. Задавайте вопросы — я постараюсь ответить развернуто в комментариях или даже написать отдельный пост. А пока вот Вам более качественная анимация демо. Собранный EXE можете скачать в репозитории.
Кто-нибудь подскажите плиз удобный и бесплатный способ захвата видео с экрана для отправки на YouTube
Что дальше
Далее мне хочется поиграться с ДНК, чтобы получить более менее красивую симуляцию. Еще нужно реализовать обработку генов джоинтов (Joint, элемент, соединяющий два узла), а также модификатор характеристик (Stats, масса и прочие физические коэффициенты, hit points и прочее). Следующее, что планируется реализовать — дополнительные элементы косточек и узлов (защитный покров, некие атакующие элементы и пр.), а также возможность разрушать косточки или заменять их порталами. В итоге хочется получить что-то вроде сомосборного домика «a-la Terraria» ну или что-то типа ДОТа :).
! СПАСИБО ЗА ВНИМАНИЕ !
Автор: HomoLuden