Fukami Vol. 1: Turnip Tentacles


    Grandfather planted a turnip ... and tentacles crawled out of it.
    UPD: Gentlemen, please, unsubscribe, what is wrong with the article and how it offended someone. And then spitting flew into karma, but it’s not clear what to correct. Not for the sake of business, but I write good things ...
    Upd 2: I forgot to warn ... In the debug mode, UnityEditor displays hundreds of messages about invalid quaternions in the console (apparently they are not normalized). Because of this, in a debug, starting from some point, it starts to slow down. I have not yet understood whether my fault or bug is a feature in the Unity release. Hands will not reach in any way to post a question on the Unity3D forum.


    Introduction


    An article by Mr. FrozmatGames prompted me to hurry up with this article, which will discuss the implementation of a prototype for a game on the subject of genetics. I apologize in advance for the possible dampness of the text. Not enough time for “combing”. Send by habrapochta of anger about slaggings and other text flaws - I will try to fix everything.
    In the article I will try to briefly describe the created demo concept with a simulation of organic growth. In the beginning I will describe the very idea by which the demo was created. Then I will describe what tools I started to use and what I stopped in the end. Well, I’ll end with a description of some points in the implementation, a demonstration, as well as a list of things that I would like to implement further.

    As always, the code is posted on Github .

    Game Idea and Prototype # 1


    The main sources of inspiration for me are the worlds of the Strugatsky brothers (A., B. and J.;), as well as several articles on the hub (see list below).



    The basic idea is a game world in which everything or almost everything is filled with life, is not built, is not assembled, is not manufactured, but is born, develops and grows. All in all, a mixture of Terraria with Spore. This is if in brief about the idea of ​​the game. I won’t tell you more yet - before that you need to put your thoughts in order.
    I decided to start with the implementation of the prototype, in which it will be possible to observe the birth and development of a certain abstract plant (working title in the title of the article). In the figures below are sketches of the conceived prototype.

    This sketch shows the initial stage of development of the object. At this stage, bones and strings connecting them “grow” from the nucleus. Bones and strings can be split or new ones added from the core. As parts of the structure grow, they can change their geometry and physical characteristics.

    Division and insertion are two operations that are well described in Neuro Evolution of Augmenting Topologies . They can be encrypted with genes, which will allow me in the future to use genetic algorithms for the evolution of the world.

    Physics2D.Net + WPF Render vs Unity3D 4.3.0


    At first, it was decided to implement the prototype using Physics2D.Net. It seems to be easy to work with him (you can read a simple tutorial on the wiki-old man ):
    PhysicsEngine engine = new PhysicsEngine();
    engine.BroadPhase = new Physics2DDotNet.Detectors.SelectiveSweepDetector();
    engine.Solver = new Physics2DDotNet.Solvers.SequentialImpulsesSolver();
    

    The simulation starts and stops through the timer object ( PhysicsTimer ):
    PhysicsTimer timer = new PhysicsTimer(engine.Update, .01f);
    timer.IsRunning = true;
    


    But the possibilities, of course, are not enough. In addition, the rendering of the scene was a problem. Rendering on WriteableBitmap was used as the simplest solution (see the code in the repository ). Later, I abandoned this approach, because I suspected that there would be difficulties in replacing the rendering with something more productive. It’s better to learn something more functional right away, I decided. If someone will be interested to read about Physics2D.Net, then unsubscribe in the comments - I'll try to find the time for it.

    After the article on the Unity3D 4.3.0 release, which implements the native 2D mode, it was decided to try to implement a demo on it. Unity3D projects differ from the OOP architecture that is familiar to me for their massive use of the component approach to organizing code. Therefore, the code from the branch with Physics2D decided not to use, but to start implementation from scratch.
    As the basis of my concept, I took a 2D demo project ( this one here ). Created his sprites for the main parts of the game object and behavior scenarios. Also, various helpers implementations were added to the project.

    He suspected that with the traditional OOP approach in Unity3D it would be difficult. Polymorphism in behavioral scenarios does not work. But in general, there were not so many problems with traditional OOP ... maybe so far. When working with a solution, I use not only Mono Develop, but also Visual Studio 2012, because C # editor in it is better and more stable. Faced Mono Develop problems with auto-indentation, dragging and dropping pieces of code, auto-closing brackets and other trifles, which are quite a lot and they strain. But I have not yet learned how to debut through VS. By the way, Mono Develop has problems with debug. For some reason namespaces with enum are unloaded in the debug, because of which the types of game objects are not shown in the debug, they have to be duplicated in string variables, which I really dislike. Maybe I just don’t know any tricks or in vain brought enum'ki to a separate namespace.

    Implementation


    What is currently implemented:
    • Three types of prefabs are added: core (Core), node (Node) and bone (Bone)
    • There are several types of nodes and bones, the core is of the same type
    • Implemented loading, decoding and replication of DNA, as well as genes in its composition. See scripts Seed, DnaProcessor, GeneProcessor in the repository
    • The decrypted genes are applied to the current part of the game object when satisfying the conditions of applicability
    • The following conditions for the applicability of the gene are implemented: current position in the tree of elements of the object (± random tolerance); subtype of the parent element; gene activation time.
    • For bone and node genes, the addition of child elements has been implemented
    • Scripts that implement various stages of growth communicate with each other by sending broadcast messages (GameObject.SendMessage) for the components of the current GameObject.
    • Implemented a simple animation when adding nodes.
    • Two singleton-helpers for working with genes (GenesManager) and prefabs in resources (PrefabsManager) are implemented.

    Now a little more detailed life cycle in the demo.
    The root of the cultivated object is the core (prefab Core). This object has a Seed script that loads from the resources of "dna.txt".
    using UnityEngine;
    using System.Collections;
    using Fukami.Helpers;
    public class Seed : MonoBehaviour
    {
                    public string Dna;
                    void Start(){
                            var dnaAsset = Resources.Load("dna");
                            Dna = dnaAsset.text.Replace("\r\n","*");
                    }
                    void OnSeedDnaStringRequested (Wrap dna)
                    {
                                    dna.Value = Dna;
                                    dna.ValueSource = "Seed";
                    }
    }


    This file contains the complete turnip DNA.
    // 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


    Also, the DnaProcessor script is attached to the kernel, which at the start checks if the current element has a parent.
    void Start()
    {
        _age = 0.0f;
        var dna = new Wrap();
        if (gameObject.transform.parent != null) {
            var gen = new Wrap();
             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;
        }
    }


    If there is no parent, DnaProcessor sends an OnSeedDnaStringRequested message requesting DNA from the Seed script. Otherwise, the message “OnDnaStringRequested” will be sent to receive DNA from the same DnaProcessor in the parent element. DnaProcessor also asks the parent for the current position in the tree (depth, generation) to pass this value to the genes.
    The life cycle of a gene is controlled by the GeneProcessor script. This script is very simple, it counts the activation time and then sends the message “OnApplyGene”.
    void Update () {
            if (Gene != null)
            {
                _age += Time.deltaTime;
            }
            if (_age >= Gene.GrowTime)
            {
                enabled = false;
                SendMessage("OnApplyGene", Gene);
                return;
            }
    }


    This message will be accepted either "BonesApplicant" (if the current element is a node), or "NodesApplicant" in case of a bone. Both of these scripts in the OnApplyGene message handler check the conditions and rules of applicability of the gene.
    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;
             }
            }


    If everything is in order and there are free slots ( ChildSlot list ), then a child is created for the gene.
    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();
            var slide = newBody.AddComponent();
            slide.connectedBody = gameObject.GetComponent();
            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();
            hinge.connectedBody = gameObject.GetComponent();
            hinge.connectedAnchor = new Vector2(slot.X, slot.Y);
            hinge.limits = new JointAngleLimits2D { min = -1.0f, max = 1.0f };
            hinge.useLimits = true;
            newBody.AddComponent();
            gameObject.SendMessage("OnChildAdded", newBody, SendMessageOptions.DontRequireReceiver);
            }


    The child element is instantiated through the PrefabsManager, which selects the prefab by the string type ( bone or node ) and the postfix of the prefab subtype. It turns out, for example, "node1", "node2", etc. The DieIfNoChildren script is additionally attached to the nodes, which simply destroys the node if a child is not attached to it after a certain time.

    About many points there is something to tell separately. Ask questions - I will try to answer in detail in the comments or even write a separate post. In the meantime, here's a better demo animation for you. You can download the assembled EXE in the repository .
    Caution! GIF 8MB
    image

    Someone please tell me a convenient and free way to capture video from the screen for sending to YouTube

    What's next


    Next, I want to play with DNA to get a more or less beautiful simulation. Still need to implement the processing of the genes of the joints (Joint, an element connecting two nodes), as well as a modifier of characteristics (Stats, mass and other physical coefficients, hit points, etc.). The next thing that is planned to be implemented is additional elements of the bones and nodes (protective cover, some attacking elements, etc.), as well as the ability to destroy bones or replace them with portals. In the end, I want to get something like a “a-la Terraria” cat-built house or something like a bunker.

    ! THANKS FOR ATTENTION !

    Also popular now: