1
\$\begingroup\$

Need some help and suggestions how to create a data tree from a text file. I'll try to explain as better as I can the setup I intend to use and currently have.

Game classes in my game are populated reading from external files, these files are simple text files with a custom "syntax", is nothing complicated, is just variables, not trying to do any interpreter for some custom scripting language. I do have the parsing and assigning object already done, my difficulty at the moment is to parse sub classes or nested ones getting the text inside the brackets.

this is how an entity text file looks like:

[NM.Game.Weapon]
{
   id = weapons/pistol;
   index = 1;
   isLocked = False;
   slotIndex = 1;
   raiseTime = 2;
   lowerTime =2;
   fireMode = 1;

   relativePosition = 0.5,0,0.25
   idleAnim = weapons/pistol_idle

   [NM.Game.Weapon+Attack, primaryFire]
   {
       fireType = 0;
       fireState = 0;
       bulletCount = 1;
       clipSize = 15;
       reloadTime = 1;
       fireDelay = 0;
       fireRate = 0.25,0.26;
       fireSpread = -0.02,0.02;
       fireRange = 500;
       projSpeed = 0;
       hitRadius = 0;
       //kickBack = 0,0,1,25;
       recoil = 0,1,0,20;
       ammoIndex = 1;
       ammoRequired = 1;
       ammoCounter = aCounter1;
       barrelOffset = 0,0,0;
       damageDef = damage/pistol0;
       fireAnim = weapons/pistol_fire1;
       reloadAnim = weapons/pistol_fire1;
       fireSfx = sfx/weapons/pistol_fire;
   }

   [NM.Game.Weapon+Attack, secondaryFire]
   {
       fireType = 0;
       fireState = 0;
       bulletCount = 1;
       clipSize = 5;
       reloadTime = 1;
       fireDelay = 0;
       fireRate = 0.5,0.55;
       fireSpread = -0.02,0.02;
       fireRange = 500;
       projSpeed = 0;
       hitRadius = 0;
       ammoIndex = 2;
       ammoRequired = 1;
       ammoCounter = aCounter2;
       barrelOffset = 0,0,0;
       damageDef = damage/pistol0;
       fireAnim = weapons/pistol_fire1;
       reloadAnim = weapons/pistol_fire1;
       fireSfx = sfx/weapons/pistol_fire;
       impactDef = impact/blt_mtl,impact/blt_conc;
   }

   [NM.Game.Weapon+Attack, tertiaryFire]
   {
       fireType = 0;
       fireState = 0;
       bulletCount = 1;
       clipSize = 5;
       reloadTime = 1;
       fireDelay = 0;
       fireRate = 0.5,0.55;
       fireSpread = -0.02,0.02;
       fireRange = 500;
       projSpeed = 0;
       hitRadius = 0;
       ammoIndex = 2;
       ammoRequired = 1;
       ammoCounter = aCounter2;
       barrelOffset = 0,0,0;
       damageDef = damage/pistol0;
       fireAnim = weapons/pistol_fire1;
       reloadAnim = weapons/pistol_fire1;
       fireSfx = sfx/weapons/pistol_fire;
       impactDef = impact/blt_mtl,impact/blt_conc;
   }
}

The first [ ] brackets at the beginning is the main object that need to be created, that header is just namespace + class name. Down below can see [NM.Game.Weapon+Attack, primaryFire]. That is a nested class is namespace + class + nested then a comma and a field name. The filed name there is the name of the variable inside Weapon, to better show here is the full C# class of it:

using UnityEngine;
using NM.Engine;

namespace NM.Game
{
    [System.Serializable]
    public class Weapon : ObjectDef
    {

        public bool isLocked;                            //Se l'arma è disponibile
        public bool isFiring {get; set; }                //Se l'arma sta sparando
        public bool isReloading {get; set; }            //Se l'arma sta ricaricando

        public int slotIndex;                            //Lo slot dove aggiungere l'arma

        public enum FireMode
        {
            Single = 0,
            Auto = 1
        }

        public enum FireType                            //Tipi di fuoco
        {
            Raycast = 0,
            Spherecast = 1,
            Projectile = 2
        }

        public enum FireState                            //Priorità di fuoco
        {
            Primary = 0,
            Secondary = 1,
            Tertiary = 2
        }

        [Range(0,1)]
        public int fireMode;                            //Metodo di fuoco

        public float raiseTime;                            //Tempo per selezionare e deselezionare arma
        public float lowerTime;                            //Tempo per selezionare e deselezionare arma

        public Attack primaryFire;                        //Fuoco primario
        public Attack secondaryFire;                    //Fuoco secondario
        public Attack tertiaryFire;                        //Fuoco secondario

        public Vector3 relativePosition;                //Posizione relativa al pivot della psrite

        public SpriteAnimation idleAnim;                //Animazione idle
        public SpriteAnimation painAnim;                //Animazione pain
        public SpriteAnimation raiseAnim;                //Animazione selezione arma
        public SpriteAnimation lowerAnim;                //Animazione cambio arma

        public AudioEffect raiseSfx;                    //Suono selezione arma
        public AudioEffect lowerSfx;                    //Suono selezione arma

        //Constructor
        public Weapon ()
        {

        }

        //Constructor args
        public Weapon (string id)
        {
            string _path = NM.IO.Path.GetExistingPath (NM.IO.Path.defPath + id + NM.IO.File.nme);

            if(System.IO.File.Exists(_path))
            {
                NM.IO.Data.ReadDynamicDataFromFile (_path, this);
            }
            else
            {
                this.id = id;
                this.isLocked = true;
                NM.Framework.Exception.nullDef (_path);
            }
        }

        //Ritorna se l'arma deve essere ricaricata
        public bool NeedReloading (Attack attack, Ammo ammo)
        {
            if (attack.isReloadable)
            {
                return ammo.count.min > 0;
            }
            else
                return false;
        }

        //Ritorna attack
        public Attack GetAttack (FireState state)
        {
            if (state == FireState.Primary)
                return primaryFire;
            else if (state == FireState.Secondary)
                return secondaryFire;
            else
                return tertiaryFire;
        }

        #region ATTACK

        [System.Serializable]
        public class Attack : ObjectDef
        {
            public int fireType;                //Tipo di fuoco corrente
            public int fireState;                //Priorità di fuoco attuale

            public int bulletCount;                //Quante munizioni sparate in una volte (non consumano piu' ammo)
            public int clipSize;                //Grandezza caricatore se è un'arma ricaricabile
            public float reloadTime;            //Tempo richiesto per ricaricare

            public bool isReloadable
            {
                get
                {
                    return clipSize > -1;
                }
            }

            public float fireDelay;                //Ritardo fuoco
            public FloatRange fireRate;            //Rateo di fuoco
            public FloatRange fireSpread;        //Rosa di fuoco
            public float fireRange;                //Raggio di fuoco per i tipi Raycast = 0 e Spherecast = 1
            public float projSpeed;                //Velocità proiettile se tipo 2 Projectile = 2
            public float hitRadius;                //Raggio di azione per SphereCast

            public Vector4 kickBack;            //Direzione e magnitudo del colpo di ritorno
            public Vector4 recoil;                //Recoil del colpo se possibile

            public int ammoIndex;                //Indice ammo necessario
            public int ammoRequired;            //Quante munizioni servono per sparare
            public string ammoCounter;            //Quale elemento hud va a toccare

            public Vector3 barrelOffset;        //Punto di uscita proiettile

            public Damage damageDef;            //Tipo di danno dell'arma

            public SpriteAnimation fireAnim;    //Animazione fuoco
            public SpriteAnimation reloadAnim;    //Animazione reload

            public AudioEffect fireSfx;            //Effetto audio di fuoco
            public AudioEffect drySfx;            //Effetto audio di fuoco senza munizioni

            public Impact impactDef;            //Effetti impatto

            //Constructor
            public Attack ()
            {

            }

            //Constructor args
            public Attack (string id)
            {
                string _path = NM.IO.Path.GetExistingPath (NM.IO.Path.defPath + id + NM.IO.File.nme);

                if(System.IO.File.Exists(_path))
                {
                    NM.IO.Data.ReadDynamicDataFromFile (_path, this);
                }
                else
                    NM.Framework.Exception.nullDef (_path);
            }

            //Ritorna lo spread dell'arma da un punto di origine dato
            public Vector3 GetSpread (Transform origin)
            {
                Vector3 _direction = origin.TransformDirection(Vector3.forward);

                _direction.x += fireSpread.range;
                _direction.y += fireSpread.range;
                _direction.z += fireSpread.range;

                return _direction;
            }

            //Ritorna posizione corretta origine di fuoco
            public Vector3 GetBarrel (Transform origin)
            {
                Vector3 _direction = origin.position;

                _direction.x = _direction.x + barrelOffset.x;
                _direction.y = _direction.y + barrelOffset.y;
                _direction.z = _direction.z + barrelOffset.z;

                return _direction;
            }
        }

        #endregion
    }
}

So as you can see primaryFire is just a public Attack object. To repeat again with different words, what I'm tryign to achieve is to build a data tree of the text file above the c# script, knowing where stuff should go.

I was thinking to have a custom Document class for the parsing, so when the StreamReader find somethign specific add to the document, for example if it finds [name] without a comma thats the header of the main class so put the text block of the root in a string to be parsed later, then if it finds a [name, field] then put the text block in another string to be parsed later.

How can I write a StreamReader that take in account such structure? I'm open to suggestions also to change the syntax of my text file if something would be "easier" to use.

Hope I was clear, if anyone want to help and is not sure of something please feel free to ask for more clarification.

PS: I'd avoid using Regex as I need to do such a thing many times at scene loading depending on the number of entities.

Just for sake of completition this is how at the moment I parse the entities, mind that for now entity structure is just the header there is no nested blocks or anything, is more akin to a ini file:

static public void ReadDynamicDataFromFile(string path, object o)
{
    Dictionary<string,string> _dic = new Dictionary<string,string>();
    var _fields = o.GetType().GetFields().ToDictionary(x => x.Name);

    bool _canEvaluate = false;

    using (StreamReader sr = new StreamReader(path)) 
    {
        string _line;

        while ((_line = sr.ReadLine()) != null)
        {
            _line = _line.Clean ();

            if(!string.IsNullOrEmpty(_line))
            {
                if(_line.EndsWith(";"))
                {
                    _line = _line.TrimEnd (Parse.semiColonChar);

                    string[] _nLine = _line.Split(Parse.equalChar);

                    if(!_dic.ContainsKey(_nLine[0]))
                        _dic.Add(_nLine[0], _nLine[1]);
                }
                else if(_line.StartsWith("["))
                {
                    _canEvaluate = true;

                    string _lineType = _line.Substring (1, _line.LastIndexOf (']') - 1);

                    string[] _abstractLine = _lineType.Split (Parse.twoDotChar);

                    string _typeLine = string.Empty;

                    if (_abstractLine.Length > 1) 
                    {
                        int _pos = 0;
                        _pos = _abstractLine [0].LastIndexOf (Parse.dotChar);

                        if (_pos != -1)
                            _abstractLine [0] = _abstractLine [0].Remove (_pos, (_abstractLine [0].Length - _pos));

                        _typeLine = _abstractLine [0] + "." + _abstractLine [1].Trim();
                    } 
                    else
                        _typeLine = _abstractLine [0];

                    if(!string.IsNullOrEmpty(_typeLine))
                    {
                        Type _t = Type.GetType (_typeLine);

                        if (_t != null) 
                        {
                            if (o.GetType () != _t) 
                                _canEvaluate = false;
                        }
                        else
                            _canEvaluate = false;

                    }
                    else
                        _canEvaluate = false;
                }
            }
        }

        sr.Close();
    }

    if(!_canEvaluate)
    {
        NM.Framework.Exception.nullError ("Impossible to evaluate class type for " + o.GetType () + " at " + path);
        return;
    }

    foreach (var key in _dic)
    {
        if (_fields.ContainsKey(key.Key)) 
        {
            SetFieldValue(o, _fields[key.Key], key.Value);
        }
    }
}

The function string.Clear() is just an extension that get rid of spaces and comments.

Thanks for your time.

\$\endgroup\$
  • 4
    \$\begingroup\$ Are you responsible for the file format ? If yes : You'd better use xml or Json. If not : Ask for xml or Json. \$\endgroup\$ – Guillaume Beauvois Nov 30 '17 at 15:32
  • 4
    \$\begingroup\$ Specifically json. \$\endgroup\$ – Ed Marty Nov 30 '17 at 15:53
  • \$\begingroup\$ Whereever I ask I get the same suggestion, guess can't escape it anymore. I wanted to try to do my own thing for the future too, is also about learning some way to parse more complex formats (not that my format is that complex), but maybe I can replicate json parsing with my own format. \$\endgroup\$ – Neurological Nov 30 '17 at 15:58
  • 1
    \$\begingroup\$ @Neurological Whereever I ask I get the same suggestion -- well, yes because by adding the unnecessary JSON-breaking [...] attributes you're complicating things A LOT. not that my format is that complex -- it is complex, and what's worse it is unnecessarily complex. It is also not obvious what benefits are added by [...]? Same type information can be represented in plain JSON. \$\endgroup\$ – Igor Soloydenko Nov 30 '17 at 17:57
  • \$\begingroup\$ When I have control of the file format I default to serializing to XML (see yegor256.com/2015/11/16/json-vs-xml.html). .NET has pretty good support for it. Why reinvent the wheel? Are you more interested creating and parsing complex serialization formats, or writing games? \$\endgroup\$ – Aron Dec 3 '17 at 23:34

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.